I got the cubism code from the Cubism Demo. The timeframe by default in the demo code is 4 hours. I'm trying to reduce it to 15 min. I successfully modified the option to make it 15 min but the graph got shrinked.
Here is the JavaScript code :
var context = cubism.context()
.step(10000)
.size(1440); // Modified this to 90 to make it 15 min
d3.select("body").selectAll(".axis")
.data(["top", "bottom"])
.enter().append("div")
.attr("class", function(d) { return d + " axis"; })
.each(function(d) { d3.select(this).call(context.axis().ticks(12).orient(d)); });
d3.select("body").append("div")
.attr("class", "rule")
.call(context.rule());
d3.select("body").selectAll(".horizon")
.data(d3.range(1, 3).map(random))
.enter().insert("div", ".bottom")
.attr("class", "horizon")
.call(context.horizon().extent([-10, 10]));
context.on("focus", function(i) {
d3.selectAll(".value").style("right", i == null ? null : context.size() - i + "px");
});
// Replace this with context.graphite and graphite.metric!
function random(x) {
var value = 0,
values = [],
i = 0,
last;
return context.metric(function(start, stop, step, callback) {
start = +start, stop = +stop;
if (isNaN(last)) last = start;
while (last < stop) {
last += step;
value = Math.max(-10, Math.min(10, value + .8 * Math.random() - .4 + .2 * Math.cos(i += x * .02)));
values.push(value);
}
callback(null, values = values.slice((start - stop) / step));
}, x);
}
Here is the demo.
How to display 15 min or lower time frame in Cubism graph?
Size option defines the number of values shown in the graph so 1440 values = 1440px width. According to the docs, step set the context step in milliseconds.
So, if you want to display 15min = 900000ms, in a graph that is 1440px wide you have to use a step that is 900000/1440 = 625.
var context = cubism.context()
.step(625)
.size(1440);
Edited your fiddle
Related
I have min and max value for x-axis. If my minvalue=0 and maxvalue=23 i want my x-axis scale to range from 0-23 but due to the automatic tickinterval adjustment(eg. 2) my x-axis scale ranges from 0-24. how do i restrict it to 23 ?
You can use tickPositions or tickPositioner option, for example:
const breaks = 10;
const dataMin = 0;
const dataMax = 23;
const step = Math.round((dataMax - dataMin) / breaks * 100) / 100;
Highcharts.chart('container', {
...,
xAxis: {
tickPositioner: function() {
const positions = [];
for (var i = dataMin; i < (dataMax - step / 2); i += step) {
positions.push(Math.round(i * 100) / 100);
}
positions.push(dataMax);
return positions;
}
}
});
Live demo: http://jsfiddle.net/BlackLabel/6m4e8x0y/4927/
API Reference:
https://api.highcharts.com/highcharts/xAxis.tickPositions
https://api.highcharts.com/highcharts/xAxis.tickPositioner
I'm new to d3 and have the following code for creating the x-axis on my graph:
export const drawXAxis = (svg, timestamps, chartWidth, chartHeight) => {
console.log(chartWidth); // 885
console.log(timestamps.length); // 310
const xScale = d3.scaleLinear()
.domain([-1, timestamps.length])
.range([0, chartWidth]);
const xBand = d3.scaleBand()
.domain(
d3.range(-1, timestamps.length))
.range([0, chartWidth])
.padding(0.3);
const xAxis = d3.axisBottom()
.scale(xScale)
.tickFormat(function(d) {
const ts = moment.utc(timestamps[d]);
return ts.format('HH') + 'h';
});
const gX = svg.append("g")
.attr("class", "axis x-axis")
.attr("transform", "translate(0," + chartHeight + ")")
.call(xAxis);
return [xScale, xBand, xAxis, gX];
};
As I understand it, d3 decides on the number of ticks that appears on the X-axis.
In order to gain more control over the values appearing on the X-axis for zooming purposes, I would like to understand how d3 determines that - in this case - I have 16 ticks.
What If I want to space the ticks more evenly, for example, I want to see a tick on every 12 or 6 hours? My data contains 0 -> 23 hour values per day consistently, but d3 displays random hours on my graph.
I'm gonna answer just the question in the title ("how is the number of ticks on an axis defined?"), not the one you made at the end ("What If I want to space the ticks more evenly, for example, I want to see a tick on every 12 or 6 hours?"), which is not related and quite simple to fix (and, besides that, it's certainly a duplicate).
Your question demands a detective work. Our journey starts, of course, at d3.axisBottom(). If you look at the source code, you'll see that the number of ticks in the enter selection...
tick = selection.selectAll(".tick").data(values, scale).order()
...depends on values, which is:
var values = tickValues == null ? (scale.ticks ? scale.ticks.apply(scale, tickArguments) : scale.domain()) : tickValues
What this line tells us is that, if tickValues is null (no tickValues used), the code should use scale.ticks for scales that have a ticks method (continuous), our just the scale's domain for ordinal scales.
That leads us to the continuous scales. There, using a linear scale (which is the one you're using), we can see at the source code that scale.ticks returns this:
scale.ticks = function(count) {
var d = domain();
return ticks(d[0], d[d.length - 1], count == null ? 10 : count);
};
However, since ticks is imported from d3.array, we have to go there for seeing how the ticks are calculated. Also, since we didn't pass anything as count, count defaults to 10.
So, finally, we arrive at this:
start = Math.ceil(start / step);
stop = Math.floor(stop / step);
ticks = new Array(n = Math.ceil(stop - start + 1));
while (++i < n) ticks[i] = (start + i) * step;
Or this:
start = Math.floor(start * step);
stop = Math.ceil(stop * step);
ticks = new Array(n = Math.ceil(start - stop + 1));
while (++i < n) ticks[i] = (start - i) / step;
Depending on the value of steps. If you look at the tickIncrement function below, you can see that steps can only be 1, 2, 5 or 10 (and their negatives).
And that's all you need to know the length of the array in the variable ticks above. Depending on the start and stop values (i.e., depending on the domain), sometimes we have more than 10 ticks (16 in your case), sometimes we have less than 10, even if the default count is 10. Have a look here:
const s = d3.scaleLinear();
console.log(s.domain([1,12]).ticks().length);
console.log(s.domain([100,240]).ticks().length);
console.log(s.domain([10,10]).ticks().length);
console.log(s.domain([2,10]).ticks().length);
console.log(s.domain([1,4]).ticks().length);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
The last example, as you can see, gives us 16 ticks.
I am trying to create a special kind of donut chart in D3 which will contain different rings for positive and negative values. The values can be greater than 100% or less than -100% so there will be an arc representing the remaining value. Below is the sample image of the chart:
The first positive category (Category_1 - Gray) value is 80, so it is 80% filling the the circle with gray, leaving the 20% for next positive category. The next positive category value (Category_2 - Orange) is 160. So it is first using the 20% left by Category_1 (140 value left now). Then it is filling the next circle (upward) 100% (40 value left now), and for the remaining value (40), it is creating partial-circle upward.
Now, we have Category_3 (dark-red) as negative (-120%), so it if creating an inward circle and filling it 100% (20 value left now), and then it is creating an inward arc for remaining value (20). We have another negative category (Category_4 - red), so it will start from where the previous negative category (Category_3) ended and fill 20% area from there.
Edit 3: I've created a very basic arc-based donut chart and when total value is exceeding 100, I am able to create outer rings for the remaining values. Below is the JSFiddle link:
http://jsfiddle.net/rishabh1990/zmuqze80/
data = [20, 240];
var startAngle = 0;
var previousData = 0;
var exceedingData;
var cumulativeData = 0;
var remainder = 100;
var innerRadius = 60;
var outerRadius = 40;
var filledFlag;
var arc = d3.svg.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius)
for (var i = 0; i < data.length; i++) {
filledFlag = 0;
exceedingData = 0;
console.log("---------- Iteration: " + (i + 1) + "---------");
if (data[i] > remainder) {
filledFlag = 1;
exceedingData = data[i] - remainder;
console.log("Exceeding: " + exceedingData);
data[i] = data[i] - exceedingData;
data.splice(i + 1, 0, exceedingData);
}
if( filledFlag === 1) {
cumulativeData = 0;
} else {
cumulativeData += data[i];
}
console.log("Previous: " + previousData);
console.log("Data: " + data, "Current Data: " + data[i]);
var endAngle = (previousData + (data[i] / 50)) * Math.PI;
console.log("Start " + startAngle, "End " + endAngle);
previousData = previousData + data[i] / 50;
//if(i===1) endAngle = 1.4 * Math.PI;
//if(i===2) endAngle = 2 * Math.PI;
var vis = d3.select("#svg_donut");
arc.startAngle(startAngle).endAngle(endAngle);
vis.append("path")
.attr("d", arc)
.attr("transform", "translate(200,200)")
.style("fill", function(d) {
if (i === 0) return "red";
//if (i === 1) return "green";
//if (i === 2) return "blue"
//if (i === 3) return "orange"
//if (i === 4) return "yellow";
});
if (exceedingData > 0) {
console.log("Increasing Radius From " + outerRadius + " To " + (outerRadius + 40));
outerRadius = outerRadius + 22;
innerRadius = innerRadius + 22;
arc.innerRadius(innerRadius).outerRadius(outerRadius);
console.log("Outer: ", outerRadius);
}
if (remainder === 100) {
remainder = 100 - data[i];
} else {
remainder = 100 - cumulativeData;
};
if (filledFlag === 1) {
remainder = 100;
}
console.log("Remainder: " + remainder);
startAngle = endAngle;
}
Please share some ideas for implementation.
Ok, this took some time, but it seems to be working. First, let's identify that what you describe as a donut chart can also be rendered as a series of bars — using the exact same data. So I started from there and eventually worked it into a donut chart, but left the bar implementation in there as well. The other thing is that a generic solution should be able to wrap the segments at any value, not just 100, so I included a slider that lets you vary that wrapping value. Finally — and this is easier to explain in a bars rather than donut implementation — rather than always having the bars wrap left-to-right, like text, it may be desirable to zigzag, i.e. alternate wrapping left-to-right then right-to-left and so on. The effect this has is that when an amount is broken up into two segments on two separate lines, the zigzag approach will keep those two segments next to each other. I added a checkbox to turn on/off this zigzag behavior.
Here's a working jsFiddle and another iteration of it.
Here are the important bits:
There's a function wrap(data, wrapLength) which takes an array of data values and a wrapLength at which to wrap these values. That function figures out which data values have to be split up into sub-segments and returns a new array of them, with each segment's object having x1, x2 and y values. x1 and x2 are the start and end of each bar, and y is the row of the bar. In a donut chart those values are equivalently start angle (x1), end angle (x2) and radius (y) of each arc.
The function wrap() doesn't know how to account for negative vs positive values, so wrap() has to be called twice — once with all the negatives and then all the positives. From there, some processing is applied selectively to just the negatives and then more processing is applied to the combination of the two sets. The entire set of transformations described in the last 2 paragraphs is captured by following snippet. I'm not including the implementation of wrap() here, just the code that calls it; also not including the rendering code, which is pretty straightforward once segments is generated.
// Turn N data points into N + x segments, as dictated by wrapLength. Do this separately
// for positive and negative values. They'll be merged further down, after we apply
// a specific transformation to just the negatives
var positiveSegments = wrap(data.filter(function(d) { return d.value > 0; }), wrapLength);
var negativeSegments = wrap(data.filter(function(d) { return d.value < 0; }), wrapLength);
// Flip and offset-by-one the y-value of every negative segment. I.e. 0 becomes -1, 1 becomes -2
negativeSegments.forEach(function(segment) { segment.y = -(segment.y + 1); });
// Flip the order of the negative segments, so that their sorted from negative-most y-value and up
negativeSegments.reverse()
// Combine negative and positive segments
segments = negativeSegments.concat(positiveSegments);
if(zigzag) {
segments.forEach(function(segment) {
if(Math.abs(segment.y) % 2 == (segment.y < 0 ? 0 : 1)) { flipSegment(segment, wrapLength); }
});
}
// Offset the y of every segment (negative or positive) so that the minimum y is 0
// and goes up from there
var maxNegativeY = negativeSegments[0].y * -1;
segments.forEach(function(segment) { segment.y += maxNegativeY; });
I'm playing around with a progress ring, and I can't seem to get it to run on a timer. I am trying to make the progress ring propagate automatically and take, say, 0.5 seconds to go from 0% to whatever percent I set (65% in this case).
I used this progress ring as a base: http://llinares.github.io/ring-progress-bar/
This is my fiddle: http://jsfiddle.net/gTtGW/
I tried using a timer function, but I may not have been integrating that properly. In the fiddle, I have added:
for (var i = 0; i< 65; i++){
range += i;
setTimeout(timer,800);
}
However, this breaks the progress ring. I thought that any time the range is updated (with the += i), the draw function would be called. What am I doing wrong? Thank you so much in advance.
If you're not planning to use the input[type=range] element, you can change your code to this:
(function (window) {
'use strict';
var document = window.document,
ring = document.getElementsByTagName('path')[0],
range = 0,
text = document.getElementsByTagName('text')[0],
Math = window.Math,
toRadians = Math.PI / 180,
r = 100;
function draw() {
// Update the wheel giving to it a value in degrees, getted from the percentage of the input value a.k.a. (value * 360) / 100
var degrees = range * 3.5999,
// Convert the degrees value to radians
rad = degrees * toRadians,
// Determine X and cut to 2 decimals
x = (Math.sin(rad) * r).toFixed(2),
// Determine Y and cut to 2 decimals
y = -(Math.cos(rad) * r).toFixed(2),
// The another half ring. Same as (deg > 180) ? 1 : 0
lenghty = window.Number(degrees > 180),
// Moveto + Arcto
descriptions = ['M', 0, 0, 'v', -r, 'A', r, r, 1, lenghty, 1, x, y, 'z'];
// Apply changes to the path
ring.setAttribute('d', descriptions.join(' '));
// Update the numeric display
text.textContent = range;
range++;
if(range > 100) {
clearInterval(timer);
}
}
// Translate the center axis to a half of total size
ring.setAttribute('transform', 'translate(' + r + ', ' + r + ')');
var timer = setInterval(draw,100);
}(this));
Basically changing range to a simple variable starting at 0, and increasing its value every time draw() is called. Creating an interval (named timer) to run every 0.1 seconds in this case (of course it's up to you), and clearing that interval from draw() when appropriate...
JSFiddle Demo
I think you want something like:
function inc() {
var val = parseInt(range.value, 10)
range.value = val + 1;
draw(); // updating the value doesn't cause `onchange`.
if (val < 100) { // don't go over 100
setTimeout(inc, 100);
}
}
inc();
I am following the scale interaction example # http://mbostock.github.com/protovis/docs/invert.html where I am trying to draw 2 line series chart.
My JSON file is as follows:
var psSeriesData =
[{"Dates":["1-10","2-10","3-10","4-10","5-10","6-10","7-10","8-10"],"ScoresForA":
[78.78,79.79,78.78,78.78,78.78,79.79,79.79,76.92],"ScoresForB":
[78.78,79.79,78.78,78.78,78.78,79.79,79.79,76.92]}]
I intend to plot the x-axis using the Dates and the 2 line chart using ScoresForA and ScoresForB respectively but am confused how to do so after much tweaking.
My code is as follows:
var data = pv.range(2).map(function(i) {
return pv.range(0, 10, .1).map(function(x) {
return {x: psSeriesData.Dates, y: psSeriesData.ScoresForA,ScoresForB };
});
});
/* Chart dimensions and scales. */
var w = 400,
h = 200,
x = pv.Scale.linear(0, 9.9).range(0, w),
y = pv.Scale.linear(0, 10).range(0, h),
i = -1;
/* The root panel. */
var vis = new pv.Panel()
.width(w)
.height(h)
.bottom(20)
.left(20)
.right(10)
.top(5);
/* Y-ticks. */
vis.add(pv.Rule)
.data(pv.range(100))
.visible(function() !(this.index % 2))
.bottom(function(d) Math.round(y(d)) - .5)
.strokeStyle(function(d) d ? "#eee" : "#000")
.anchor("left").add(pv.Label)
.text(function(d) (d * 10).toFixed(0) );
/* X-ticks. */
vis.add(pv.Rule)
.data(x.ticks())
.visible(function(d) d > 0)
.left(function(d) Math.round(x(d)) - .5)
.strokeStyle(function(d) d ? "#eee" : "#000")
.anchor("bottom").add(pv.Label)
.text(function(d) d.toFixed());
/* A panel for each data series. */
var panel = vis.add(pv.Panel)
.data(data);
/* The line. */
var line = panel.add(pv.Line)
.data(function(d) d)
.left(function(d) x(d.x))
.bottom(function(d) y(d.y))
.lineWidth(3);
/* The mouseover dots and label. */
line.add(pv.Dot)
.visible(function() i >= 0)
.data(function(d) [d[i]])
.fillStyle(function() line.strokeStyle())
.strokeStyle("#000")
.size(20)
.lineWidth(1)
.add(pv.Dot)
.left(10)
.bottom(function() this.parent.index * 12 + 10)
.anchor("right").add(pv.Label)
.text(function(d) (d.y * 10).toFixed(5));
/* An invisible bar to capture events (without flickering). */
vis.add(pv.Bar)
.fillStyle("rgba(0,0,0,.001)")
.event("mouseout", function() {
i = -1;
return vis;
})
.event("mousemove", function() {
var mx = x.invert(vis.mouse().x);
i = pv.search(data[0].map(function(d) d.x), mx);
i = i < 0 ? (-i - 2) : i;
return vis;
});
vis.render();
What am I doing wrong?
After inputs were given by nrabinowitz:
var psSeriesData = {
"Dates": ["1/10","2/10","3/10","4/10","5/10","6/10","7/10","8/10"],
"ScoresForA": [78.78,79.79,78.78,78.78,78.78,79.79,79.79,76.92],
"ScoresForB": [78.78,79.79,78.78,78.78,78.78,79.79,79.79,76.92]
};
// start by iterating over the two keys for your time series data
var data = ["ScoresForA","ScoresForB"].map(function(seriesKey) {
// use pv.range to walk through the indexes of the
// date array (basically the same as a for loop)
return pv.range(0, psSeriesData.Dates.length)
// map these indexes to an array of objects
.map(function(dateIndex) {
// now return an object with the date index
// and series value for that index
return {
x: dateIndex,
y: psSeriesData[seriesKey][dateIndex]
}
});
});
/* Chart dimensions and scales. */
var w = 400,
h = 200,
x = pv.Scale.linear(0, 9.9).range(0, w),
y = pv.Scale.linear(0, 10).range(0, h),
i = -1;
/* The root panel. */
var vis = new pv.Panel()
.width(w)
.height(h)
.bottom(20)
.left(20)
.right(10)
.top(5);
/* Y-ticks. */
vis.add(pv.Rule)
.data(pv.range(100))
.visible(function() !(this.index % 2))
.bottom(function(d) Math.round(y(d)) - .5)
.strokeStyle(function(d) d ? "#eee" : "#000")
.anchor("left").add(pv.Label)
.text(function(d) (d * 10).toFixed(0) );
/* X-ticks. */
vis.add(pv.Rule)
//.data(function(d) [d[i].Dates])
.data(pv.range(0, psSeriesData.Dates.length).map(function(a) (psSeriesData[a].Dates)))
.visible(function(d) d > 0)
.left(function(d) Math.round(x(d)) - .5)
.strokeStyle(function(d) d ? "#eee" : "#000")
.anchor("bottom").add(pv.Label)
.text(function(d) (d).toFixed());
/* A panel for each data series. */
var panel = vis.add(pv.Panel)
.data(data);
/* The line. */
var line = panel.add(pv.Line)
.data(function(d) d)
.left(function(d) x(d.x))
.bottom(function(d) y(d.y))
.lineWidth(3);
/* The mouseover dots and label. */
line.add(pv.Dot)
.visible(function() i >= 0)
.data(function(d) [d[i]])
.fillStyle(function() line.strokeStyle())
.strokeStyle("#000")
.size(20)
.lineWidth(1)
.add(pv.Dot)
.left(10)
.bottom(function() this.parent.index * 12 + 10)
.anchor("right").add(pv.Label)
.text(function(d) (d.y ).toFixed(5));
/* An invisible bar to capture events (without flickering). */
vis.add(pv.Bar)
.fillStyle("rgba(0,0,0,.001)")
.event("mouseout", function() {
i = -1;
return vis;
})
.event("mousemove", function() {
var mx = x.invert(vis.mouse().x);
i = pv.search(data[0].map(function(d) d.x), mx);
i = i < 0 ? (-i - 2) : i;
return vis;
});
vis.render();
Dates are still not displaying out as the x-axis, even though I used the map function and array referencing. There seems to be a problem reading the 'Dates' property. Any advices
Error: TypeError: Cannot read property 'Dates' of undefined
The first thing to do when working on a visualization like this (and especially when following the Protovis examples) is to make sure your data is in the format you need. I haven't gone through all of your code here, but you've got some clear issues with the data right up front:
Why is your initial data in an array? Is there any reason to include the enclosing straight braces (i.e. the outer brackets in psSeriesData = [{ ... }])? There's no reason for this that I can see in the code as you've presented it, and it's only going to confuse things (e.g. psSeriesData.Dates is undefined - you'd need to reference psSeriesData[0].Dates).
I'm not at all clear on what you're doing in your initial data-setup code, but I'm pretty certain it's not giving you what you want - it looks like a blind cut-and-paste from the example, even though it doesn't apply. The example is using pv.range to generate fake data - you don't need this, you have real data, and you can walk through this instead.
The best way to start here is to understand what the data is supposed to look like. In the example, the data is produced like this:
data = pv.range(3).map(function(i) {
return pv.range(0, 10, .1).map(function(x) {
return {x: x, y: i + Math.sin(x) + Math.random() * .5 + 2};
});
});
Run this in a console, and you'll see that the data produced looks like this:
[
[
{
x: 0.1,
y: 2.34
},
// ...
],
// ...
]
The outer array holds the diffent time series; each time series is an array of objects like {x:0.1, y:2.34}. If your data doesn't look like this, it won't work with the example code.
Your initial data should work fine for this, but you'll need to get it into the right format. One issue here is the list of dates - these are strings, not numbers, and you won't be able to use them as data unless you either convert them to Date objects (this is a real pain, avoid it if possible) or map them to numbers - the latter is easy here because they're in a regular series. (If you had unevenly spaced dates, this all would be more complex, but let's forget that for now.) You can just use the index of the dates as the x values, and then use your two series as the y values.
Putting this all together, you can format your data like this:
// note - no enclosing array
var psSeriesData = {
"Dates": ["1-10","2-10","3-10","4-10","5-10","6-10","7-10", "8-10"],
"ScoresForA": [78.78,79.79,78.78,78.78,78.78,79.79,79.79,76.92],
"ScoresForB": [78.78,79.79,78.78,78.78,78.78,79.79,79.79,76.92]
};
// start by iterating over the two keys for your time series data
var data = ["ScoresForA","ScoresForB"].map(function(seriesKey) {
// use pv.range to walk through the indexes of the
// date array (basically the same as a for loop)
return pv.range(0, psSeriesData.Dates.length)
// map these indexes to an array of objects
.map(function(dateIndex) {
// now return an object with the date index
// and series value for that index
return {
x: dateIndex,
y: psSeriesData[seriesKey][dateIndex]
}
});
});
There are a lot of other ways to do this as well, but the main point is that you come out with an array like this: [[{x:0, y:79.79}, ...], ...]. I haven't looked at the rest of your code, but now that your data is in the correct format, you should be able to replace the fake data in the example with the real data in your code, and have the whole thing work as expected (though you'll need to change any assumptions in the example about the expected max and min values for x and y).