I'm trying to learn SVG graphs, I've nearly completed my graph however I currently have overlapping bars, I believe this is due to a combination of my x axis return data and my json data. My x axis lists the months from my JSON data, but my JSON data can have multiple entries from the same month. and if this is the case then my entries will overlap for that month. What I need to do is return the month + day seperately, however I haven't been able to figure out how just yet.
JSON data example where there's 2 entries for the same month:
1: {dayNo: 2, monthNo: 7, monthName: "July", year: 2018, recordDate: "2018-07-02T00:00:00",…}
avgVolume: 3585.53
dayNo: 2
monthName: "July"
monthNo: 7
recordDate: "2018-07-02T00:00:00"
volumeLastYear: 4109.47
volumeToday: 3575.34
year: 2018
2: {dayNo: 30, monthNo: 7, monthName: "July", year: 2018, recordDate: "2018-07-30T00:00:00",…}
avgVolume: 3291.55
dayNo: 30
monthName: "July"
monthNo: 7
recordDate: "2018-07-30T00:00:00"
volumeLastYear: 3996.31
volumeToday: 3414.44
year: 2018
And here is the svg code:
$request.done(function (response) {
// remove old svg
$('svg').remove();
// create svg element
var $svg = d3.select('.card.mb-3.clickable-card.highlight')
.append('svg')
.attr('width', '100%')
.attr('height', '400px')
.attr('style', 'background-color: lavender')
.classed('svg display-svg', true);
// 2 determine size of svg element
var w = $svg.node().getBoundingClientRect().width;
var h = $svg.node().getBoundingClientRect().height;
// 10 chart margins
var chartMargin = new Object();
chartMargin.left = 40;
chartMargin.right = 25;
chartMargin.top = 25;
chartMargin.bottom = 40;
// 10 cont
h = h - chartMargin.bottom - chartMargin.top;
// Max volume
var maxVolume = d3.max(response, d => d.avgVolume);
// bar Margins / width
var barMargin = 10;
var barWidth = (w - chartMargin.left - chartMargin.right) / response.length;
// yScale
var yScale = d3.scaleLinear()
.domain([0, maxVolume])
.range([h, chartMargin.top]);
// xScale
var xScale = d3.scaleBand()
.domain(response.map(function (d) { return d.monthName; }))
.range([0, w - chartMargin.left - chartMargin.right])
.paddingInner(0.15);
// chartGroup
var chartGroup = $svg.append('g')
.classed('chartGroup', true)
.attr('transform', 'translate(' + chartMargin.left + ',' + chartMargin.top + ')');
// 5
var barGroups = chartGroup
.selectAll('g')
.data(response);
// 6
var newBarGroups = barGroups.enter()
.append('g')
.attr('transform', function (d, i) {
return 'translate('
+ (xScale(d.monthName))
+ ','
+ (yScale(d.avgVolume))
+ ')';
})
// yAxis label
var yAxis = d3.axisLeft(yScale);
chartGroup.append('g')
.classed('axis y', true)
.attr('transform', 'translate(' + -20 + ', ' + h / 2 + ')')
.append('text')
.attr('transform', 'rotate(-90)')
.attr('dx', '-0.8em')
.attr('dy', '0.25em')
.attr("text-anchor", 'middle')
.text("Reservoir Volume (ML)");
// xAxis label
var xAxis = d3.axisBottom(xScale);
chartGroup.append('g')
.attr('transform', 'translate(0,' + h + ')')
.classed('axis x', true)
.call(xAxis);
newBarGroups
.append('rect')
.attr('x', 0)
.attr('height', function (d, i) {
return h - yScale(d.avgVolume);
})
// animation
.style('fill', 'transparent')
.transition().duration(function (d, i) { return i * 500; })
.delay(function (d, i) { return i + 200; })
.attr('width', barWidth - barMargin)
.style('fill', function (d, index) {
return "hsl(240, 100%, " + (d.avgVolume / maxVolume * 80) + "%)"
});
// bar text
newBarGroups
.append('text')
.attr('transform', 'rotate(-45)')
.attr('text-anchor', 'middle')
.attr('x', function () { return 0; })
.attr('y', 50)
.attr('fill', 'white')
.text(function (d, i) { return d.avgVolume; })
.style('font-size', '1em')
});
in the xScale, I have tried changing the return data from
return d.monthName
to
return d.monthName + ' ' + d.dayNo
But then all my bars overlap and I get this error for each g element:
d3.js:1211 Error: attribute transform: Expected number, "translate(undefined,25.183…".
Here is what I have currently with returning d.monthName:
Here is d.monthName + ' ' + d.dayNo:
Nevermind sorry, I fixed it
I needed to change
var newBarGroups = barGroups.enter()
.append('g')
.attr('transform', function (d, i) {
return 'translate('
+ (xScale(d.monthName))
+ ','
+ (yScale(d.avgVolume))
+ ')';
})
to
var newBarGroups = barGroups.enter()
.append('g')
.attr('transform', function (d, i) {
return 'translate('
+ (xScale(d.monthName + ' ' + d.dayNo))
+ ','
+ (yScale(d.avgVolume))
+ ')';
})
Related
I would like to display text on y-axis tick point they are like always same even if data changed here are the tick point text ["Low" , "Medium" , "High"]
i have tried a lot but im unable to find the perfect solution
tick points are alway 3 and they have to show this text on each tick point ["Low" , "Medium" , "High"]
here is the image that how i want
this is what i have implemented
im unable to set the text of ["Low" , "Medium" , "High"] by replacing [5, 10, 15]
here is the array of data for plotting graph
[
{ group: 'A', value: 5 },
{ group: 'B', value: 15 },
{ group: 'C', value: 10 },
{ group: 'D', value: 15 },
{ group: 'E', value: 10 },
{ group: 'F', value: 5 },
]
here is the bar chart code
var outerWidth = convertRemToPixels(40),
outerHeight = barGraphContainerRef.current.clientHeight;
var margin = {
top: convertRemToPixels(2),
right: convertRemToPixels(2),
bottom: convertRemToPixels(2),
left: convertRemToPixels(2),
},
width = outerWidth - margin.left - margin.right,
height = outerHeight - margin.top - margin.bottom;
var x = d3.scaleBand().range([0, width]);
var y = d3.scaleLinear().range([height, 0]);
var xAxis = d3.axisBottom(x);
var yAxis = d3.axisLeft(y).ticks(3);
function make_y_gridlines() {
return d3.axisLeft(y).ticks(3);
}
//Defenining the tooltip div
var chart = d3
.select(refe.current)
.attr('width', outerWidth)
.attr('height', outerHeight);
let tooltip = d3
.select('#root .tooltipContainer')
.style('position', 'absolute')
.style('top', 0)
.style('left', 0)
.style('display', 'none');
var gradient = chart
.select('.lGradient')
.attr('id', 'gradient')
.attr('x1', '0%')
.attr('y1', '00%')
.attr('x2', '0%')
.attr('y2', '100%')
.attr('spreadMethod', 'pad');
gradient
.select('.firstStop')
.attr('offset', '-100%')
.attr('stop-color', '#0170ac')
.attr('stop-opacity', 1);
gradient
.select('.secondStop')
.attr('offset', '100%')
.attr('stop-color', '#013c5d')
.attr('stop-opacity', 1);
var main = chart
.select('.chart')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
main
.select('.x-axis')
.attr('transform', 'translate(0,' + height + ')')
.call(xAxis)
.style('color', '#a4a4a4');
main
.select('.y-axis')
.attr('transform', 'translate(0,' + 0 + ')')
.call(yAxis)
.style('color', '#a4a4a4');
y.domain([
0,
d3.max(graphData, function (d) {
return d.value;
}),
]);
x.domain(
graphData.map(function (d) {
console.log(d);
return d.group;
})
).padding([0.5]);
main
.select('.x-axis')
.call(xAxis)
.selectAll('text')
.style('text-anchor', 'end')
.attr('dx', '.3em')
.attr('dy', '1em')
.style('font-size', '.8rem')
.attr('transform', 'rotate(0)')
.style('font-family', '"Roboto", sans-serif');
main
.select('.y-axis')
.call(yAxis)
.selectAll('text')
.attr('class', 'yAxiesText')
.attr('transform', 'rotate(-90)')
.attr('y', '-2em')
.attr('x', '.4em')
.attr('dy', '.71em')
.style('font-size', '.8rem')
.style('text-anchor', 'end')
.style('font-family', '"Roboto", sans-serif');
main
.select('.gridder')
.call(make_y_gridlines().tickSize(-width).tickFormat(''))
.attr('id', 'gridSystem');
var rects = main.selectAll('.paths').data(graphData.map(e => e.value));
const names = graphData.map(e => e.group);
rects
.join('path')
.attr('class', 'paths')
.attr('d', function (d, i) {
return RectangleBarWithRadiusOneSide(
x(names[i]),
y(d),
x.bandwidth(),
height - y(d),
5 // radius
);
})
here is the RectangleBarWithRadiusOneSide function for making onside border radius
const RectangleBarWithRadiusOneSide = (x, y, width, height, radius) => {
return (
'M' +
(x + radius) +
',' +
y +
'h' +
(width - 2 * radius) +
'a' +
radius +
',' +
radius +
' 0 0 1 ' +
radius +
',' +
radius +
'v' +
(height - 2 * radius) +
'v' +
radius +
'h' +
-radius +
'h' +
(2 * radius - width) +
'h' +
-radius +
'v' +
-radius +
'v' +
(2 * radius - height) +
'a' +
radius +
',' +
radius +
' 0 0 1 ' +
radius +
',' +
-radius +
'z'
);
};
here is the convertRemToPixels function
export const convertRemToPixels = rem => {
return rem * parseFloat(getComputedStyle(document.documentElement).fontSize);
};
I hope i have explained meaningfully
if i get any solution from the anyone that is so appreciatable
thanks advance!
You can use the tickFormat() function on a d3 axis generator to format the tick values. Create a scale to map the data values to your "Low", "High", "Medium" labels and pass it to tickFormat():
const tickScale = d3.scaleOrdinal()
.domain([...new Set(data.map(d => d.value))].sort(d3.ascending)) // keep unique values in ascending order
.range(["Low", "Medium", "High"])
.unknown(""); // other values are not displayed
const yAxis = d3.axisLeft(sy)
.ticks(3)
.tickFormat(tickScale);
A working prototype can be found here: https://codepen.io/ccasenove/pen/abKNOqa
I am using d3.js for a zoomable candlestick chart.It's working fine but I need to write a tool tip for showing OHLC values. I did some reference to try, but I failed. Can somebody help me to debug? Thank you very much!
Here is my code and it is very long, but for tooltips part, it's in the middle part.
function drawChart() {
d3.csv("amazon.csv").then(function(prices) {
const months = {0 : 'Jan', 1 : 'Feb', 2 : 'Mar', 3 : 'Apr', 4 : 'May', 5 : 'Jun', 6 : 'Jul', 7 : 'Aug', 8 : 'Sep', 9 : 'Oct', 10 : 'Nov', 11 : 'Dec'}
var dateFormat = d3.timeParse("%Y-%m-%d");
for (var i = 0; i < prices.length; i++) {
prices[i]['Date'] = dateFormat(prices[i]['Date'])
}
const margin = {top: 15, right: 65, bottom: 205, left: 50},
w = 1000 - margin.left - margin.right,
h = 625 - margin.top - margin.bottom;
var svg = d3.select("#container")
.attr("width", w + margin.left + margin.right)
.attr("height", h + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" +margin.left+ "," +margin.top+ ")");
let dates = _.map(prices, 'Date');
var xmin = d3.min(prices.map(r => r.Date.getTime()));
var xmax = d3.max(prices.map(r => r.Date.getTime()));
var xScale = d3.scaleLinear().domain([-1, dates.length])
.range([0, w])
var xDateScale = d3.scaleQuantize().domain([0, dates.length]).range(dates)
let xBand = d3.scaleBand().domain(d3.range(-1, dates.length)).range([0, w]).padding(0.3)
var xAxis = d3.axisBottom()
.scale(xScale)
.tickFormat(function(d) {
d = dates[d]
// hours = d.getHours()
// minutes = (d.getMinutes()<10?'0':'') + d.getMinutes()
// amPM = hours < 13 ? 'am' : 'pm'
// return hours + ':' + minutes + amPM + ' ' + d.getDate() + ' ' + months[d.getMonth()] + ' ' + d.getFullYear()
return d.getDate() + ' ' + months[d.getMonth()] + ' ' + d.getFullYear()
});
svg.append("rect")
.attr("id","rect")
.attr("width", w)
.attr("height", h)
.style("fill", "none")
.style("pointer-events", "all")
.attr("clip-path", "url(#clip)")
var gX = svg.append("g")
.attr("class", "axis x-axis") //Assign "axis" class
.attr("transform", "translate(0," + h + ")")
.call(xAxis)
gX.selectAll(".tick text")
.call(wrap, xBand.bandwidth())
var ymin = d3.min(prices.map(r => r.Low));
var ymax = d3.max(prices.map(r => r.High));
var yScale = d3.scaleLinear().domain([ymin, ymax]).range([h, 0]).nice();
var yAxis = d3.axisLeft()
.scale(yScale)
var gY = svg.append("g")
.attr("class", "axis y-axis")
.call(yAxis);
var chartBody = svg.append("g")
.attr("class", "chartBody")
.attr("clip-path", "url(#clip)");
// draw rectangles
let candles = chartBody.selectAll(".candle")
.data(prices)
.enter()
.append("rect")
.attr('x', (d, i) => xScale(i) - xBand.bandwidth())
.attr("class", "candle")
.attr('y', d => yScale(Math.max(d.Open, d.Close)))
.attr('width', xBand.bandwidth())
.attr('height', d => (d.Open === d.Close) ? 1 : yScale(Math.min(d.Open, d.Close))-yScale(Math.max(d.Open, d.Close)))
.attr("fill", d => (d.Open === d.Close) ? "silver" : (d.Open > d.Close) ? "red" : "green")
// draw high and low
let stems = chartBody.selectAll("g.line")
.data(prices)
.enter()
.append("line")
.attr("class", "stem")
.attr("x1", (d, i) => xScale(i) - xBand.bandwidth()/2)
.attr("x2", (d, i) => xScale(i) - xBand.bandwidth()/2)
.attr("y1", d => yScale(d.High))
.attr("y2", d => yScale(d.Low))
.attr("stroke", d => (d.Open === d.Close) ? "white" : (d.Open > d.Close) ? "red" : "green");
// -1- Create a tooltip div that is hidden by default:
let tooltip = d3.select("#container")
.append("div")
.style("opacity", 0)
.attr("class", "tooltip")
.style("background-color", "black")
.style("border-radius", "5px")
.style("padding", "10px")
.style("color", "white");
// -2- Create 3 functions to show / update (when mouse move but stay on same circle) / hide the tooltip
let showTooltip = function(d) {
tooltip
.transition()
.duration(200);
tooltip
.style("opacity", 1)
.html("Open: " + d.Open)
.html("High: " + d.High)
.html("Low: " + d.Low)
.html("Close: " + d.Close)
.style("left", (d3.mouse(this)[0]+30) + "px")
.style("top", (d3.mouse(this)[1]+30) + "px")
};
let moveTooltip = function() {
tooltip
.style("left", (d3.mouse(this)[0]+30) + "px")
.style("top", (d3.mouse(this)[1]+30) + "px")
};
let hideTooltip = function() {
tooltip
.transition()
.duration(200)
.style("opacity", 0)
};
svg.append("defs")
.append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("width", w)
.attr("height", h)
.on("mouseover", showTooltip )// -3- Trigger the functions
.on("mousemove", moveTooltip )
.on("mouseleave", hideTooltip);
const extent = [[0, 0], [w, h]];
var resizeTimer;
var zoom = d3.zoom()
.scaleExtent([1, 100])
.translateExtent(extent)
.extent(extent)
.on("zoom", zoomed)
.on('zoom.end', zoomend);
svg.call(zoom);
function zoomed() {
var t = d3.event.transform;
let xScaleZ = t.rescaleX(xScale);
let hideTicksWithoutLabel = function() {
d3.selectAll('.xAxis .tick text').each(function(d){
if(this.innerHTML === '') {
this.parentNode.style.display = 'none'
}
})
};
gX.call(
d3.axisBottom(xScaleZ).tickFormat((d, e, target) => {
if (d >= 0 && d <= dates.length-1) {
d = dates[d];
// hours = d.getHours()
// minutes = (d.getMinutes()<10?'0':'') + d.getMinutes()
// amPM = hours < 13 ? 'am' : 'pm'
// return hours + ':' + minutes + amPM + ' ' + d.getDate() + ' ' + months[d.getMonth()] + ' ' + d.getFullYear()
return d.getDate() + ' ' + months[d.getMonth()] + ' ' + d.getFullYear()
}
})
);
candles.attr("x", (d, i) => xScaleZ(i) - (xBand.bandwidth()*t.k)/2)
.attr("width", xBand.bandwidth()*t.k);
stems.attr("x1", (d, i) => xScaleZ(i) - xBand.bandwidth()/2 + xBand.bandwidth()*0.5);
stems.attr("x2", (d, i) => xScaleZ(i) - xBand.bandwidth()/2 + xBand.bandwidth()*0.5);
hideTicksWithoutLabel();
gX.selectAll(".tick text")
.call(wrap, xBand.bandwidth())
}
function zoomend() {
var t = d3.event.transform;
let xScaleZ = t.rescaleX(xScale);
clearTimeout(resizeTimer);
resizeTimer = setTimeout(function() {
var xmin = new Date(xDateScale(Math.floor(xScaleZ.domain()[0])));
xmax = new Date(xDateScale(Math.floor(xScaleZ.domain()[1])));
filtered = _.filter(prices, d => ((d.Date >= xmin) && (d.Date <= xmax)));
minP = +d3.min(filtered, d => d.Low);
maxP = +d3.max(filtered, d => d.High);
buffer = Math.floor((maxP - minP) * 0.1);
yScale.domain([minP - buffer, maxP + buffer]);
candles.transition()
.duration(800)
.attr("y", (d) => yScale(Math.max(d.Open, d.Close)))
.attr("height", d => (d.Open === d.Close) ? 1 : yScale(Math.min(d.Open, d.Close))-yScale(Math.max(d.Open, d.Close)));
stems.transition().duration(800)
.attr("y1", (d) => yScale(d.High))
.attr("y2", (d) => yScale(d.Low));
gY.transition().duration(800).call(d3.axisLeft().scale(yScale));
}, 500)
}
});
}
function wrap(text, width) {
text.each(function() {
var text = d3.select(this),
words = text.text().split(/\s+/).reverse(),
word,
line = [],
lineNumber = 0,
lineHeight = 1.1, // ems
y = text.attr("y"),
dy = parseFloat(text.attr("dy")),
tspan = text.text(null).append("tspan").attr("x", 0).attr("y", y).attr("dy", dy + "em");
while (word = words.pop()) {
line.push(word);
tspan.text(line.join(" "));
if (tspan.node().getComputedTextLength() > width) {
line.pop();
tspan.text(line.join(" "));
line = [word];
tspan = text.append("tspan").attr("x", 0).attr("y", y).attr("dy", ++lineNumber * lineHeight + dy + "em").text(word);
}
}
});
}
drawChart();
In my d3 bar chart, I should have Y-axis dynamic ticks and grid lines for integer values (no ticks and grid lines for decimal values). But its not working , Please help me on this
var itemData =[{month: "MARCH", year: 2018, number: 26, date:1519842600000},{month: "MAY", year: 2018, number: 34, date: 1525113000000}];
createChart(itemData )
createChart(itemData) {
const router_temp = this.router;
const element = this.chartContainer.nativeElement;
const factoryId = itemData['id'];
const data = itemData.data;
d3.select(element).selectAll('*').remove();
const div = d3.select('body').append('div')
.attr('class', 'tooltip-bar-chart')
.style('display', 'none');
const svg = d3.select(element),
margin = {top: 10, right: 10, bottom: 20, left: 30},
width = +this.el.nativeElement.offsetWidth - margin.left - margin.right,
height = +svg.attr('height') - margin.top - margin.bottom;
const x = d3.scaleBand().range([10, width - 10]).padding(1),
y = d3.scaleLinear().rangeRound([height, 0]);
// add the X gridlines
svg.append('g')
.attr('class', 'grid')
.attr('transform', 'translate(30,' + height + ')')
.call(make_x_gridlines()
.tickSize(-height)
.tickFormat('')
);
// add the Y gridlines
svg.append('g')
.attr('class', 'grid')
.attr('transform', 'translate(30, 10)')
.call(make_y_gridlines()
.tickSize(-width)
.tickFormat('')
);
const g = svg.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
x.domain(data.map(function(d) { return d.month; }));
y.domain([0, d3.max(data, function(d) { return d.number; })]);
//
g.selectAll('.bar')
.data(data)
.enter().append('rect')
.attr('class', 'bar')
.attr('x', function(d) { return x(d.month); })
.attr('y', function(d) { return y(d.number); })
.attr('width', 12)
.attr('height', function(d) { return height - y(d.number); })
.on('mouseover', function(d) {
div.style('display', 'block');
div.html(
'<div class=\'main\'>' +
'<div class=\'month\'>' +
d.month +
'</div>' +
'<div class=\'number\'>' +
d.number +
'</div>' +
'<div class=\'bottom\'>' +
'Number of Applications Transformed & Migrated' +
'</div>' +
'</div>'
)
.style('left', (d3.event.pageX + 15) + 'px')
.style('top', (d3.event.pageY - 50) + 'px');
})
.on('mouseout', function(d) {
div.style('display', 'none');
})
const dataLength = data.length;
const xPositions = [];
for (let i = 0; i < dataLength; i += 1) {
xPositions.push(x(data[i].month) + margin.left);
}
const newX = d3.scaleOrdinal().range(xPositions);
const xScale = newX.domain(itemData.xlabels);
svg.append('g')
.attr('class', 'axis axis--x')
.attr('transform', 'translate(0,' + (height + 10) + ')')
.call(d3.axisBottom(xScale));
g.append('g')
.attr('class', 'axis axis--y')
.call(d3.axisLeft(y).ticks(5));
// gridlines in x axis function
function make_x_gridlines() {
return d3.axisBottom(x)
.ticks(1);
}
// gridlines in y axis function
function make_y_gridlines() {
return d3.axisLeft(y)
.ticks(5);
}
}
}
Here is sample
I created a fiddle.
that while I had to change a few things to account for no angular, but shows the axis gridlines and labels aligned. I moved everything into one g group is is then translated and the individual components are no longer translated.
I am trying to modfiy the example stacked area chart here
currently it gives me an error as such:
Error: <path> attribute d: Expected number, "MNaN,22770LNaN,21…".
at this line: .attr('d', area);
This is my code so far:
let margin = {top: 20, right: 60, bottom: 30, left: 60},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
let parseDate = d3.timeParse('%m/%d/%Y');
let x = d3.scaleTime().range([0, width]),
y = d3.scaleLinear()
.range([height, 0]),
z = d3.scaleOrdinal()
.domain(['Detractors', 'Passives' , 'Promoters'])
.range(['#e81123', '#a0a1a2', '#7fba00']);
let stack = d3.stack();
let area = d3.area()
.x(function(d) { return x(d.data.date); })
.y0(function(d) { return y(d.y0); })
.y1(function(d) { return y(d.y0 + d.y); });
let g = this._svg.append('g')
.attr('transform', 'translate(' + 0 + ',' + margin.top + ')');
let data = [
{ 'date': '04/23/12' , 'Detractors': 20 , 'Passives': 30 , 'Promoters': 50 },
{ 'date': '04/24/12' , 'Detractors': 32 , 'Passives': 19 , 'Promoters': 42 },
{ 'date': '04/25/12' , 'Detractors': 45 , 'Passives': 11 , 'Promoters': 44 },
{ 'date': '04/26/12' , 'Detractors': 20 , 'Passives': 13 , 'Promoters': 64 }];
// console.log(myData.map(function (d) { return d.key; }));
// console.log('KEYS: '+ keys);
x.domain(d3.extent(data, function(d) {
console.log(parseDate(d.date));
return parseDate(d.date); }));
// let keys = (d3.keys(data[0]).filter(function (key) { return key !== 'date'; }));
z.domain(d3.keys(data[0]).filter(function (key) { return key !== 'date'; }));
// z.domain(keys);
stack.keys(d3.keys(data[0]).filter(function (key) { return key !== 'date'; }));
let layer = g.selectAll('.layer')
.data(stack(data))
.enter().append('g')
.attr('class', 'layer');
layer.append('path')
.attr('class', 'area')
.style('fill', function(d) {
// console.log('d.key: ' + d.key);
console.log('d.key: ' + d.key + ' color: ' + z(d.key));
return z(d.key); })
.attr('d', area);
layer.filter(function(d) { return d[d.length - 1][1] - d[d.length - 1][0] > 0.01; })
.append('text')
.attr('x', width - 6)
.attr('y', function(d) { return y((d[d.length - 1][0] + d[d.length - 1][1]) / 2); })
.attr('dy', '.35em')
.style('font', '10px sans-serif')
.style('text-anchor', 'end')
.text(function(d) {
// console.log('key label: ' + d.key);
return d.key; });
g.append('g')
.attr('class', 'axis axis--x')
.attr('transform', 'translate(0,' + height + ')')
.call(d3.axisBottom(x));
// console.log(height);
g.append('g')
.attr('class', 'axis axis--y')
.call(d3.axisLeft(y).ticks(5, '%'));
}
I am just trying to parse the array so I can have the date, keys and value parsing properly. I just can't seem to get it right.
Much help is appreciated!!
The specific error that you are getting is due to the format of the date in your data. You have a line commented out in your code: let parseDate = d3.timeParse("%m/%d/%Y"); You do want to use the d3.timeParse function to transform your data to date format:
let tp = d3.timeParse("%m/%d/%y")
for(var i=0; i<data.length;i++){
data[i].date = tp(data[i].date)
}
I'm building a sort of timeline tool using d3.js that can be zoomed and paged forward and back within a set time period. I have my axis transitioning perfectly but I can't seem to get my data to reload. The idea is that the zoom level defines a moving window, for example 1 hour that the user moves backwards and forwards over a period of a few days. Every time they do so a call is made to fetch the data for that hour.
Apologies for the large code block, the methods that handle the fetching of new data are towards the bottom. If it makes any difference the tool is a directive for angular.js.
scope.zoomLevels = [
{
value: 1,
unit: 'minutes',
tickFormat: $window.d3.time.format('%S')
},
{
value: 30,
unit: 'minutes',
tickFormat: $window.d3.time.format('%H:%M')
},
{
value: 1,
unit: 'hours',
tickFormat: $window.d3.time.format('%H:%M')
},
{
value: 3,
unit: 'hours',
tickFormat: $window.d3.time.format('%H:%M')
},
{
value: 6,
unit: 'hours',
tickFormat: $window.d3.time.format('%H:%M')
},
{
value: 12,
unit: 'hours',
tickFormat: $window.d3.time.format('%H:%M')
},
{
value: 1,
unit: 'days',
tickFormat: $window.d3.time.format('%H:%M')
}
];
var margin = {top: 0, right: 0, bottom: 0, left: 0},
width = $window.innerWidth,
height = $window.innerHeight - margin.top - margin.bottom,
xAxisHeight = $window.innerHeight - margin.top - ($window.innerHeight/2);
var start = $window.moment(Datasets.current.dates[0], 'DD/MM/YYYY'),
end = $window.moment(Datasets.current.dates[Datasets.current.dates.length - 1], 'DD/MM/YYYY'),
duration = $window.moment.duration(end.diff(start));
scope.zoomLevel = 3;
scope.page = 0;
scope.maxPage = duration.asMinutes()/scope.zoomLevels[scope.zoomLevel].value;
//$scope.tweets = Tweets.getTweets(start.format('x'), end.format('x'));
var rows = Math.floor(height/ 60);
console.log(rows + ' Rows');
var x = $window.d3.time.scale()
.domain([start.toDate(), start.clone().add(scope.zoomLevels[scope.zoomLevel].value, scope.zoomLevels[scope.zoomLevel].unit).toDate()])
.range([0, width]);
var y = $window.d3.scale.linear()
.domain([(rows/2) * -1,(rows/2)])
.range([height, 0]);
var xAxis = $window.d3.svg.axis()
.scale(x);
var yAxis = $window.d3.svg.axis()
.scale(y)
.tickSize(5)
.orient('right');
var svg = $window.d3.select('#timeline').append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
svg.append('g')
.attr('class', 'x axis')
.attr('transform', 'translate(0,' + xAxisHeight + ')')
.call(xAxis)
.selectAll('text')
.attr('y', 15)
.attr('x', 0)
.attr('dy', '.35em');
svg.append('g')
.attr('class', 'y axis')
.call(yAxis);
Articles.get({startDate: start.format(), endDate: start.clone().add(scope.zoomLevels[scope.zoomLevel].value, scope.zoomLevels[scope.zoomLevel].unit).format(), limit: 20}, function(data){
var bar = svg.selectAll('.news')
.data(data.items)
.enter()
.append('g')
.attr('class', 'news')
.on('mouseover', function(d) {
console.log(d);
})
.on('mouseout', function(d) {
});
bar.append('rect')
.attr('width', 250)
.attr('height', 60)
.attr('x', function(d){return x($window.moment(d.date, 'YYYY-MM-DDTHH:mm:ss.000Z').format('x'));})
.attr('y', function(d){return y(2);})
bar.append('foreignObject')
.attr('width', 250)
.attr('height', 60)
.attr('x', function(d){return x($window.moment(d.date, 'YYYY-MM-DDTHH:mm:ss.000Z').format('x'));})
.attr('y', function(d){return y(2);})
.append('xhtml:p')
.attr("class","statement")
.text(function(d) { return d.title; });
});
scope.movePage = function(direction){
if(!(direction === 1 && scope.page === scope.maxPage) || !(direction === -1 && scope.page === 0)){
console.log('Paging allowed');
if(direction === 1){
start = start.add(scope.zoomLevels[scope.zoomLevel].value, scope.zoomLevels[scope.zoomLevel].unit);
x.domain([start.toDate(), start.clone().add(scope.zoomLevels[scope.zoomLevel].value, scope.zoomLevels[scope.zoomLevel].unit).toDate()]);
Articles.get({startDate: start.format(), endDate: start.clone().add(scope.zoomLevels[scope.zoomLevel].value, scope.zoomLevels[scope.zoomLevel].unit).format(), limit: 20}, function(data) {
console.log(data.items.length);
svg.selectAll('.news').data(data.items).transition().duration(1500).ease('sin-in-out');
svg.selectAll('g.axis.x').transition().duration(1500).ease('sin-in-out').call(xAxis);
});
scope.page++;
}
if(direction === -1){
start = start.subtract(scope.zoomLevels[scope.zoomLevel].value, scope.zoomLevels[scope.zoomLevel].unit);
x.domain([start.toDate(), start.clone().add(scope.zoomLevels[scope.zoomLevel].value, scope.zoomLevels[scope.zoomLevel].unit).toDate()]);
svg.selectAll('g.axis.x').transition().duration(1500).ease('sin-in-out').call(xAxis);
scope.page--;
}
}
};
scope.toggleZoom = function(direction){
if(!(direction === 1 && scope.zoomLevel === 6) || !(direction === -1 && scope.zoomLevel === 0)){
duration = $window.moment.duration(end.diff(start));
scope.zoomLevel = scope.zoomLevel + direction;
console.log(scope.zoomLevels[scope.zoomLevel].unit);
switch (scope.zoomLevels[scope.zoomLevel].unit){
case 'minutes':
scope.maxPage = duration.asMinutes()/scope.zoomLevels[scope.zoomLevel].value;
console.log(duration.asMinutes() + ' Minutes');
console.log((duration.asMinutes()/scope.zoomLevels[scope.zoomLevel].value) + ' Pages');
break;
case 'hours':
scope.maxPage = duration.asHours()/scope.zoomLevels[scope.zoomLevel].value;
console.log(duration.asHours() + ' Hours');
console.log((duration.asHours()/scope.zoomLevels[scope.zoomLevel].value) + ' Pages');
break;
case 'days':
scope.maxPage = duration.asDays()/scope.zoomLevels[scope.zoomLevel].value;
console.log(duration.asDays() + ' Days');
console.log((duration.asDays()/scope.zoomLevels[scope.zoomLevel].value) + ' Pages');
break;
}
x.domain([start.toDate(), start.clone().add(scope.zoomLevels[scope.zoomLevel].value, scope.zoomLevels[scope.zoomLevel].unit).toDate()]);
svg.selectAll('g.axis').transition().duration(1500).ease('sin-in-out').call(xAxis);
}
};
Update #1
I've been able to get new data to appear and older data to be removed using the following code in the Articles.get callback after line console.log(data.items.length)
var news = svg.selectAll(".news").data(data.items);
// Add new element
news.enter()
.append("g")
.attr("class", "news")
.transition()
.duration(1500)
.ease("sin-in-out");
news.append('rect')
.attr('width', 250)
.attr('height', 60)
.attr('x', function(d){return x($window.moment(d.date, 'YYYY-MM-DDTHH:mm:ss.000Z').format('x'));})
.attr('y', function(d){return y(2);});
news.append('foreignObject')
.attr('width', 250)
.attr('height', 60)
.attr('x', function(d){return x($window.moment(d.date, 'YYYY-MM-DDTHH:mm:ss.000Z').format('x'));})
.attr('y', function(d){return y(2);})
.append('xhtml:p')
.attr("class","statement")
.text(function(d) { return d.title; });
// Remove old elements
news.exit()
.attr("class", "news")
.transition()
.duration(1500)
.ease("sin-in-out")
.remove();
However the animations don't really work. Ultimately I want the svg elements that represent each news item to move to the left at the same pace the axis moves if the user clicks next page. Currently they appear/disappear instantly as the user clicks the forward and back buttons.
if(!(direction === 1 && scope.page === scope.maxPage) || !(direction === -1 && scope.page === 0)){
if(direction === 1){ ... }
if(direction === -1){ .. }
This is not easy to parse, are you sure it's possible for either of the inner code blocks to execute given those conditionals?