display months on time axis with d3 - d3.js

I am replicating the Githubs contributions heatmap
I have successfully created and colorized the heatmap, but I am having problems creating the time scale that would only show the months or even better month and year. Any tips on how to display months? This is what I have so far:
let dateRange = d3.timeDay.range(new Date(2022, 0, 1), new Date()).map(e => { return { "date": e, "value": Math.floor(Math.random() * 15) } });
const cellSize = 16
const countDay = d => d.getUTCDay()
const timeWeek = d3.utcSunday;
const colorFn = d3.scaleSequential(d3.interpolateHcl( "#2be5ba", "#1d846c")).domain([
Math.floor(d3.min(dateRange, (o)=>o.value)),
Math.ceil(d3.max(dateRange, (o)=>o.value))
])
d3.select("#heatmap")
.append("svg")
.append('g')
.selectAll('rect')
.data(dateRange)
.join('rect')
.attr("width", cellSize - 1.5)
.attr("height", cellSize - 1.5)
.attr("x", (d, i) => timeWeek.count(d3.timeYear(d.date), d.date) * cellSize + 10)
.attr("y", d => countDay(d.date) * cellSize + 0.5)
.attr("fill", d => {
if(d.value == 0){
return "#e5edf3";
return "aliceblue";
}else{
return colorFn(d.value);
}})
.attr("stroke", d => {
if(d.value == 0){
return null;
return "#dbe2e8";
return "aliceblue";
}else{
return null;
}})
.attr("rx", "2")

Related

how to make this D3js sunburst chart display all layers of data?

My D3js sunburst chart is only displaying 2 layers of data even though there are more layers.
I want it to display all layers of data
A live version of the chart is here: D3 Chart
The code is below. please advise on how to make the chart display all of the layers of data.
I need to see more than just two layers.
export default function define(runtime, observer) {
const main = runtime.module();
main.variable(observer()).define(["md"], function(md){return(
md`# D3 Zoomable Sunburst
This variant of a [sunburst diagram](/#mbostock/d3-sunburst), a radial orientation of D3’s [hierarchical partition layout](https://github.com/d3/d3-hierarchy/blob/master/README.md#partition), shows only two layers of the [Flare visualization toolkit](https://flare.prefuse.org) package hierarchy at a time. Click a node to zoom in, or click the center to zoom out.`
)});
main.variable(observer("chart")).define("chart", ["partition","data","d3","DOM","width","color","arc","format","radius"], function(partition,data,d3,DOM,width,color,arc,format,radius)
{
const root = partition(data);
root.each(d => d.current = d);
const svg = d3.select(DOM.svg(width, width))
.style("width", "100%")
.style("height", "auto")
.style("font", "10px sans-serif");
const g = svg.append("g")
.attr("transform", `translate(${width / 2},${width / 2})`)
.on("mouseleave",mouseleave);
const path = g.append("g")
.selectAll("path")
.data(root.descendants().slice(1))
.enter().append("path")
.attr("fill", d => { while (d.depth > 1) d = d.parent; return color(d.data.name); })
.attr("fill-opacity", d => arcVisible(d.current) ? (d.children ? 0.6 : 0.4) : 0)
.attr("d", d => arc(d.current))
.on("mouseover",mouseover);
path.filter(d => d.children)
.style("cursor", "pointer")
.on("click", clicked);
path.append("title")
.text(d => `${d.ancestors().map(d => d.data.name).reverse().join("/")}\n${format(d.value)}`);
const label = g.append("g")
.attr("pointer-events", "none")
.attr("text-anchor", "middle")
.style("user-select", "none")
.selectAll("text")
.data(root.descendants().slice(1))
.enter().append("text")
.attr("dy", "0.35em")
.attr("fill-opacity", d => +labelVisible(d.current))
.attr("transform", d => labelTransform(d.current))
.text(d => d.data.name);
//percentage text
const percentage_text=svg.append("text")
.attr("id","title")
.attr("x", (width / 2))
.attr("y", (width / 2))
.attr("text-anchor", "middle")
.style("font-size", "2.5em");
const parent = g.append("circle")
.datum(root)
.attr("r", radius)
.attr("fill", "none")
.attr("pointer-events", "all")
.on("click", clicked);
function clicked(p) {
parent.datum(p.parent || root);
root.each(d => d.target = {
x0: Math.max(0, Math.min(1, (d.x0 - p.x0) / (p.x1 - p.x0))) * 2 * Math.PI,
x1: Math.max(0, Math.min(1, (d.x1 - p.x0) / (p.x1 - p.x0))) * 2 * Math.PI,
y0: Math.max(0, d.y0 - p.depth),
y1: Math.max(0, d.y1 - p.depth)
});
const t = g.transition().duration(750);
// Transition the data on all arcs, even the ones that aren’t visible,
// so that if this transition is interrupted, entering arcs will start
// the next transition from the desired position.
path.transition(t)
.tween("data", d => {
const i = d3.interpolate(d.current, d.target);
return t => d.current = i(t);
})
.filter(function(d) {
return +this.getAttribute("fill-opacity") || arcVisible(d.target);
})
.attr("fill-opacity", d => arcVisible(d.target) ? (d.children ? 0.6 : 0.4) : 0)
.attrTween("d", d => () => arc(d.current));
label.filter(function(d) {
return +this.getAttribute("fill-opacity") || labelVisible(d.target);
}).transition(t)
.attr("fill-opacity", d => +labelVisible(d.target))
.attrTween("transform", d => () => labelTransform(d.current));
}
//mouse over
const totalSize = root.descendants()[0].value;
function mouseover(d){
var percentage = (100 * d.value / totalSize).toPrecision(3);
var percentageString = percentage + "%";
if (percentage < 0.1) {
percentageString = "< 0.1%"; }
percentage_text.text(percentageString);
var sequenceArray = d.ancestors().reverse();
sequenceArray.shift(); // remove root node from the array
// Fade all the segments.
d3.selectAll("path")
.style("opacity", 0.3);
// Then highlight only those that are an ancestor of the current segment.
g.selectAll("path")
.filter(function(node) {
return (sequenceArray.indexOf(node) >= 0);
})
.style("opacity", 1);
}
//mouse leave
// Restore everything to full opacity when moving off the visualization.
function mouseleave(d) {
// Deactivate all segments during transition.
//d3.selectAll("path").on("mouseover", null);
// Transition each segment to full opacity and then reactivate it.
d3.selectAll("path")
.transition()
.duration(200)
.style("opacity", 1)
.on("end", function() {
d3.select(this).on("mouseover", mouseover);
});
percentage_text.text("");
}
function arcVisible(d) {
return d.y1 <= 3 && d.y0 >= 1 && d.x1 > d.x0;
}
function labelVisible(d) {
return d.y1 <= 3 && d.y0 >= 1 && (d.y1 - d.y0) * (d.x1 - d.x0) > 0.03;
}
function labelTransform(d) {
const x = (d.x0 + d.x1) / 2 * 180 / Math.PI;
const y = (d.y0 + d.y1) / 2 * radius;
return `rotate(${x - 90}) translate(${y},0) rotate(${x < 180 ? 0 : 180})`;
}
return svg.node();
}
);
main.variable(observer("data")).define("data", ["d3"], async function(d3){return(
await d3.json("data.json")
)});
main.variable(observer("partition")).define("partition", ["d3"], function(d3){return(
data => {
const root = d3.hierarchy(data)
.sum(d => d.size)
.sort((a, b) => b.value - a.value);
return d3.partition()
.size([2 * Math.PI, root.height + 1])
(root);
}
)});
main.variable(observer("color")).define("color", ["d3","data"], function(d3,data){return(
d3.scaleOrdinal().range(d3.quantize(d3.interpolateRainbow, data.children.length + 1))
)});
main.variable(observer("format")).define("format", ["d3"], function(d3){return(
d3.format(",d")
)});
main.variable(observer("width")).define("width", function(){return(
974
)});
main.variable(observer("radius")).define("radius", ["width"], function(width){return(
width / 6
)});
main.variable(observer("arc")).define("arc", ["d3","radius"], function(d3,radius){return(
d3.arc()
.startAngle(d => d.x0)
.endAngle(d => d.x1)
.padAngle(d => Math.min((d.x1 - d.x0) / 2, 0.005))
.padRadius(radius * 1.5)
.innerRadius(d => d.y0 * radius)
.outerRadius(d => Math.max(d.y0 * radius, d.y1 * radius - 1))
)});
main.variable(observer("d3")).define("d3", ["require"], function(require){return(
require("d3#5")
)});
main.variable(observer()).define(["partition","data"], function(partition,data){return(
partition(data).descendants()[1]
)});
return main;
}
I've made a couple of changes, firstly making the example more minimal by removing hover and click events.
I needed to change the arc radius and the partition method as per this example, and took text positioning logic from this fiddle.
const width = 500,
radius = width / 2,
format = d3.format(",d");
const svg = d3.select('svg')
.attr("width", width)
.attr("height", width)
.style("font", "10px sans-serif");
const g = svg.append("g")
.attr("transform", `translate(${radius},${radius})`);
const arc = d3.arc()
.startAngle(d => d.x0)
.endAngle(d => d.x1)
.padAngle(d => Math.min((d.x1 - d.x0) / 2, 0.005))
.padRadius(radius)
.innerRadius(d => Math.sqrt(d.y0))
.outerRadius(d => Math.sqrt(d.y1) - 1);
d3.json("https://gist.githubusercontent.com/MargretWG/b3f9e0a383408c6e6a45fc652e83a26c/raw/8756e2320d05a774e96983234beff81b01409315/hierarchy.json").then(data => {
const root = partition(data);
root.each(d => d.current = d);
const color = d3.scaleOrdinal().range(d3.quantize(d3.interpolateRainbow, data.children.length + 1));
const path = g.append("g")
.selectAll("path")
.data(root.descendants().slice(1))
.enter().append("path")
.attr("fill", d => {
while (d.depth > 1) d = d.parent;
return color(d.data.name);
})
.attr("fill-opacity", d => d.children ? 0.6 : 0.4)
.attr("d", d => arc(d.current));
path.append("title")
.text(d => `${d.ancestors().map(d => d.data.name).reverse().join("/")}\n${format(d.value)}`);
const label = g.append("g")
.attr("pointer-events", "none")
.attr("text-anchor", "middle")
.style("user-select", "none")
.selectAll("text")
.data(root.descendants().slice(1))
.enter().append("text")
.attr("dy", "0.35em")
.attr("transform", (d) => `translate(${arc.centroid(d)}) rotate(${getAngle(d)})`)
.text(d => d.data.name);
});
const partition = data => {
const root = d3.hierarchy(data)
.sum(d => d.size)
.sort((a, b) => b.value - a.value);
return d3.partition()
.size([2 * Math.PI, radius * radius])
(root);
}
function getAngle(d) {
// Offset the angle by 90 deg since the '0' degree axis for arc is Y axis, while
// for text it is the X axis.
var thetaDeg = (180 / Math.PI * (arc.startAngle()(d) + arc.endAngle()(d)) / 2 - 90);
// If we are rotating the text by more than 90 deg, then "flip" it.
// This is why "text-anchor", "middle" is important, otherwise, this "flip" would
// a little harder.
return (thetaDeg > 90) ? thetaDeg - 180 : thetaDeg;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg></svg>

D3.js + waffle chart + partially fill circle/ rectangle in waffle chart based on the data

I am trying to fill circles in waffle partially based on the data. It should show circles partially coloured. With my following code, I am getting black circles instead of partially coloured circles.
The clip-path I am trying to draw is incorrect or so.
Any help will be appreciated.
Thank you in advance!
const waffle = d3.select(waffleDiv)
.append('svg')
.attr('width', width)
.attr('height', height)
.append('g')
.selectAll('div')
.data(theData)
.enter()
.append('rect')
.attr('width', squareSize)
.attr('height', squareSize)
.attr('rx', (squareSize / 2))
.attr('ry', (squareSize / 2))
.attr('fill', function (d, i) {
const len = theData.length;
const current = theData[i];
// const previous = theData[(i + len - 1) % len];
const next = theData[(i + 1) % len];
if (i < theData.length) {
if (current.groupIndex !== next.groupIndex) {
if (d.lastUnitValue < 100) {
const currentCircle = d3.select(this);
// currentCircle.attr('fill', color(d.groupIndex));
const svgEle = d3.select('svg');
const cpath = svgEle.append('clipPath')
.attr('id', 'circle-clip');
cpath.append('rect')
.attr('width', squareSize)
.attr('height', squareSize)
.attr('rx', (squareSize / 2))
.attr('ry', (squareSize / 2));
// .attr('fill', color(d.groupIndex));
currentCircle.append('rect')
.attr('width', squareSize)
.attr('height', squareSize - ((squareSize * d.lastUnitValue) / 100))
.attr('rx', (squareSize / 2))
.attr('ry', (squareSize / 2))
.attr('clip-path', 'url(#circle-clip)')
.attr('fill', 'white');
}
} else {
return color(d.groupIndex);
}
}
})
.attr('x', function (d, i) {
// group n squares for column
const col = Math.floor(i / heightSquares);
return (col * squareSize) + (col * gap);
})
.attr('y', function (d, i) {
const row = i % heightSquares;
return ((heightSquares * squareSize) - ((row * squareSize) + (row * gap)) - 5);
})
.attr('stroke', 'lightgray')
.attr('stroke-width', 1)
.append('title')
.text(function (d, i) {
// return 'Total members: ' + d.ageGroup + ' | ' + d.value + ' , ' + (d.units / theData.length) * 100 + '%';
return 'Members count: ' + d.categories + ' | ' + d.actualValue.toLocaleString();
})

D3 - Create a chart with 3 fixed y-axis ticks

I have a D3 chart but only want to show 3 ticks for the y-axis.
Depending on the data, I sometimes get 3, 4 or 5 ticks which makes it difficult for me to style with CSS.
Here is the full code:
// Create a new d3
var chart = d3.select('#analytics-chart').append('div').attr('class', 'chart');
chart.append('div').attr('class', 'y-axis');
chart.append('div').attr('class', 'bars-and-x-axis');
var barMargin = '0 2px',
min = 0,
max = d3.max(data, function(d) {
return parseInt(d.value, 10);
});
var bars = d3.selectAll('.bars-and-x-axis').append('div').attr('class', 'bars'),
xaxis = d3.selectAll('.bars-and-x-axis').append('div').attr('class', 'x-axis'),
yaxis = d3.selectAll('.y-axis'),
xScale = d3.scale.linear().domain([1, data.length]),
yScale = d3.scale.linear().range(0, 100).domain([min, max]),
barWrapper = bars.selectAll()
.data(data.map(function(d) {
return d.value;
}))
.enter()
.append('div')
.attr('class', function(d, i) {
if (d == 0) {
return 'chart-data-wrapper empty';
} else {
return 'chart-data-wrapper';
}
}).style('margin', barMargin);
var bar = barWrapper.append('div').attr('class', 'chart-data-bar')
.style('height', function(d) {
return Math.ceil((d - min) / (max - min) * 100) + 'px';
})
.append('div')
.attr('class', 'tooltip')
.attr('style', function(d, i) {
return 'left: ' + Math.ceil(i / data.length * 100) + '%; transform: translateX(-' + Math.ceil(i / data.length * 100) + '%); ';
})
.append('p')
.text(function(d, i) {
return data[i].date;
})
.append('p')
.attr('class', 'data')
.text(function(d, i) {
return data[i].content;
});
xaxis.selectAll()
.data(xScale.ticks(12))
.enter()
.append('div')
.attr('class', 'x-axis-mark');
yaxis.selectAll()
.data(yScale.ticks(3))
.enter()
.insert('small', ':first-child')
.attr('class', 'label')
.text(function(d, i) {
if (d > 999) {
d = d / 1000 + 'k';
}
return d;
});
var tick = d3.selectAll('.x-axis-mark')
.append('div')
.attr('class', function(d, i) {
if (i % 3 == 1) {
return 'x-axis-tick-with-label';
} else {
return 'x-axis-tick';
}
});
var label = d3.selectAll('.x-axis-mark')
.append('small')
.attr('class', 'label')
.text(function(d, i) {
var format = d3.time.format('%b');
return data[i].xlabel;
})
.attr('class', function(d, i) {
if (i % 3 != 1) {
d3.select(this).remove();
}
});
Edit - I've added the full code and attached an image of the working example below:
I was able to replace yScale.ticks(3) in .data(yScale.ticks(3)) with my own math function that returns an array of values to create the tick values.

Add arrow to both sides of line in D3

I am new for D3. How can I add an arrow to the beginning and to the end of this line?
var x = 100;
var y = 100;
var canvas = d3.select("#canvasContainer")
.append("svg")
.attr("width", 600)
.attr("height", 500);
var line1 = canvas.append("line")
.attr("x1", x)
.attr("y1", y)
.attr("x2", x)
.attr("y2", y + 50)
.attr("stroke", "red")
.attr("stroke-width", "3");
All you need is:
svg
.append('svg:defs')
.append('svg:marker')
.attr('id', 'triangle')
.attr('refX', 6)
.attr('refY', 6)
.attr('markerWidth', 30)
.attr('markerHeight', 30)
.attr('markerUnits', 'userSpaceOnUse')
.attr('orient', 'auto')
.append('path')
.attr('d', 'M 0 0 12 6 0 12 3 6')
.style('fill', 'black');
Next:
const pathNodes = svg
.append('path')
.datum(path)
.attr('d', line)
.attr('fill', 'none')
.attr('stroke-width', '3')
.attr('stroke', (d) => d.color)
.attr('marker-end', 'url(#triangle)');
Or with animation in my case:
const arrow = svg
.append('svg:path')
.attr('d', 'M 0 0 12 6 0 12 3 6')
.attr('fill', 'black');
arrow
.transition()
.duration(2000)
.delay(1500)
.ease(d3.easeLinear)
.attrTween('transform', this.arrowAnimation(pathNodes.node()));
arrowAnimation(path) => {
const l = path.getTotalLength();
let prevX = 0;
let prevY = 0;
return (d, i, a) => {
return (t) => {
const p = path.getPointAtLength(t * l);
const deltaY = prevY - p.y;
const deltaX = prevX - p.x;
prevY = p.y;
prevX = p.x;
let rotTran;
if (deltaY === 0) {
if (deltaX > 0) {
rotTran = 'rotate(-180)';
} else {
rotTran = 'rotate(180)';
}
} else if (deltaX === 0) {
if (deltaY > 0) {
rotTran = 'rotate(-90)';
} else {
rotTran = 'rotate(90)';
}
} else {
rotTran = 'rotate(0)';
}
return 'translate(' + p.x + ',' + p.y + ') ' + rotTran;
};
};
}

d3.js: why doesn't the data update?

I've got a fairly simple reusable chart built in D3.js -- some circles and some text.
I'm struggling to figure out how to update the chart with new data, without redrawing the entire chart.
With the current script, I can see that the new data is bound to the svg element, but none of the data-driven text or attributes is updating. Why isn't the chart updating?
Here's a fiddle: http://jsfiddle.net/rolfsf/em5kL/1/
I'm calling the chart like this:
d3.select('#clusters')
.datum({
Name: 'Total Widgets',
Value: 224,
Clusters: [
['Other', 45],
['FooBars', 30],
['Foos', 50],
['Bars', 124],
['BarFoos', 0]
]
})
.call( clusterChart() );
When the button is clicked, I'm simply calling the chart again, with different data:
$("#doSomething").on("click", function(){
d3.select('#clusters')
.datum({
Name: 'Total Widgets',
Value: 122,
Clusters: [
['Other', 14],
['FooBars', 60],
['Foos', 22],
['Bars', 100],
['BarFoos', 5]
]
})
.call( clusterChart() );
});
The chart script:
function clusterChart() {
var width = 450,
margin = 0,
radiusAll = 72,
maxRadius = radiusAll - 5,
r = d3.scale.linear(),
padding = 1,
height = 3 * (radiusAll*2 + padding),
startAngle = Math.PI / 2,
onTotalMouseOver = null,
onTotalClick = null,
onClusterMouseOver = null,
onClusterClick = null;
val = function(d){return d};
function chart(selection) {
selection.each(function(data) {
var cx = width / 2,
cy = height / 2,
stepAngle = 2 * Math.PI / data.Clusters.length,
outerRadius = 2*radiusAll + padding;
r = d3.scale.linear()
.domain([0, d3.max(data.Clusters, function(d){return d[1];})])
.range([50, maxRadius]);
var svg = d3.select(this).selectAll("svg")
.data([data])
.enter().append("svg");
//enter
var totalCircle = svg.append("circle")
.attr("class", "total-cluster")
.attr('cx', cx)
.attr('cy', cy)
.attr('r', radiusAll)
.on('mouseover', onTotalMouseOver)
.on('click', onTotalClick);
var totalName = svg.append("text")
.attr("class", "total-name")
.attr('x', cx)
.attr('y', cy + 16);
var totalValue = svg.append("text")
.attr("class", "total-value")
.attr('x', cx)
.attr('y', cy + 4);
var clusters = svg.selectAll('circle.cluster')
.data(data.Clusters)
.enter().append('circle')
.attr("class", "cluster");
var clusterValues = svg.selectAll("text.cluster-value")
.data(data.Clusters)
.enter().append('text')
.attr('class', 'cluster-value');
var clusterNames = svg.selectAll("text.cluster-name")
.data(data.Clusters)
.enter().append('text')
.attr('class', 'cluster-name');
clusters .attr('cx', function(d, i) { return cx + Math.cos(startAngle + stepAngle * i) * outerRadius; })
.attr('cy', function(d, i) { return cy + Math.sin(startAngle + stepAngle * i) * outerRadius; })
.attr("r", "10")
.on('mouseover', function(d, i, j) {
if (onClusterMouseOver != null) onClusterMouseOver(d, i, j);
})
.on('mouseout', function() { /*do something*/ })
.on('click', function(d, i){ onClusterClick(d); });
clusterNames
.attr('x', function(d, i) { return cx + Math.cos(startAngle + stepAngle * i) * outerRadius; })
.attr('y', function(d, i) { return cy + Math.sin(startAngle + stepAngle * i) * outerRadius + 16; });
clusterValues
.attr('x', function(d, i) { return cx + Math.cos(startAngle + stepAngle * i) * outerRadius; })
.attr('y', function(d, i) { return cy + Math.sin(startAngle + stepAngle * i) * outerRadius - 4; });
//update with data
svg .selectAll('text.total-value')
.text(val(data.Value));
svg .selectAll('text.total-name')
.text(val(data.Name));
clusters
.attr('class', function(d, i) {
if(d[1] === 0){ return 'cluster empty'}
else {return 'cluster'}
})
.attr("r", function (d, i) { return r(d[1]); });
clusterValues
.text(function(d) { return d[1] });
clusterNames
.text(function(d, i) { return d[0] });
$(window).resize(function() {
var w = $('.cluster-chart').width(); //make this more generic
svg.attr("width", w);
svg.attr("height", w * height / width);
});
});
}
chart.width = function(_) {
if (!arguments.length) return width;
width = _;
return chart;
};
chart.onClusterClick = function(_) {
if (!arguments.length) return onClusterClick;
onClusterClick = _;
return chart;
};
return chart;
}
I have applied the enter/update/exit pattern across all relevant svg elements (including the svg itself). Here is an example segment:
var clusterValues = svg.selectAll("text.cluster-value")
.data(data.Clusters,function(d){ return d[1];});
clusterValues.exit().remove();
clusterValues
.enter().append('text')
.attr('class', 'cluster-value');
...
Here is a complete FIDDLE with all parts working.
NOTE: I tried to touch your code as little as possible since you have carefully gone about applying a re-usable approach. This the reason why the enter/update/exit pattern is a bit different between the total circle (and text), and the other circles (and text). I might have gone about this using a svg:g element to group each circle and associated text.

Resources