D3 y-axis percent display - javascript

D3 Fiddle
/**
* A bar chart. Required data format:
* [ { name : x-axis-bar-label, value : N }, ...]
*
* Sample use:
* var bargraph = d3.select('#bargraph')
* .append('svg')
* .chart('BarChart')
* .yFormat(d3.format("d"))
* .height(400)
* .width(800)
* .max(1.0);
* bargraph.draw(bardata);
*/
d3.chart('BarChart', {
initialize: function() {
var chart = this;
// chart margins to account for labels.
// we may want to have setters for this.
// not sure how necessary that is tbh.
chart.margins = {
top : 10,
bottom : 15,
left : 50,
right : 0,
padding : 10
};
// default chart ranges
chart.x = d3.scale.linear();
chart.y = d3.scale.linear();
chart.base
.classed('Barchart', true);
// non data driven areas (as in not to be independatly drawn)
chart.areas = {};
// cache for selections that are layer bases.
chart.layers = {};
// make sections for labels and main area
// _________________
// |Y| bars |
// | | |
// | | |
// | | |
// | |______________|
// | X |
// -- areas
chart.areas.ylabels = chart.base.append('g')
.classed('ylabels', true)
.attr('width', chart.margins.left)
.attr('transform',
'translate('+(chart.margins.left-1)+','+(chart.margins.top + 1)+')');
// -- actual layers
chart.layers.bars = chart.base.append('g')
.classed('bars', true)
.attr('transform',
'translate(' + chart.margins.left + ',' + (chart.margins.top + 1)+')');
chart.layers.xlabels = chart.base.append('g')
.classed('xlabels', true)
.attr('height', chart.margins.bottom);
chart.on("change:width", function() {
chart.x.range([0, chart.w - chart.margins.left]);
chart.layers.bars.attr('width', chart.w - chart.margins.left);
chart.layers.xlabels.attr('width', chart.w - chart.margins.left);
});
chart.on("change:height", function() {
chart.y.range([chart.h - chart.margins.bottom - chart.margins.top, 0]);
chart.areas.ylabels
.attr('height', chart.h - chart.margins.bottom - chart.margins.top - 1);
chart.layers.bars
.attr('height', chart.h - chart.margins.bottom - chart.margins.top);
chart.layers.xlabels
.attr('transform', 'translate(' + chart.margins.left + ',' +
(chart.h - chart.margins.bottom + 1) + ')');
});
// make actual layers
chart.layer('bars', chart.layers.bars, {
// data format:
// [ { name : x-axis-bar-label, value : N }, ...]
dataBind : function(data) {
chart.data = data;
// how many bars?
chart.bars = data.length;
// compute box size
chart.bar_width = (chart.w - chart.margins.left - ((chart.bars - 1) *
chart.margins.padding)) / chart.bars;
// adjust the x domain - the number of bars.
chart.x.domain([0, chart.bars]);
// adjust the y domain - find the max in the data.
chart.datamax = chart.usermax ||
d3.extent(data, function(d) { return d.value; })[1];
chart.y.domain([0, chart.datamax]);
// draw yaxis
var yAxis = d3.svg.axis()
.scale(chart.y)
.orient('left')
.ticks(5)
.tickFormat(chart._yformat || d3.format('.0%'));
chart.areas.ylabels
.call(yAxis);
return this.selectAll('rect')
.data(data, function(d) {
//console.log(d);
return d.name;
});
},
insert : function() {
return this.append('rect')
.classed('bar', true)
.classed('highlight', function(d) {
return d.highlight;
});
},
events: {
exit: function() {
this.remove();
}
}
});
// a layer for the x text labels.
chart.layer('xlabels', chart.layers.xlabels, {
dataBind : function(data) {
// first append a line to the top.
this.append('line')
.attr('x1', 0)
.attr('x2', chart.w - chart.margins.left)
.attr('y1', 0)
.attr('y2', 0)
.style('stroke', '#222')
.style('stroke-width', '1')
.style('shape-rendering', 'crispEdges');
return this.selectAll('text')
.data(data, function(d) { return d.name; });
},
insert : function() {
return this.append('text')
.classed('label', true)
.attr('text-anchor', 'middle')
.attr('x', function(d, i) {
return chart.x(i) - 0.5 + chart.bar_width/2;
})
.attr('dy', '1em')
.text(function(d) {
return d.name;
});
},
events: {
exit: function() {
this.remove();
}
}
});
// on new/update data
// render the bars.
var onEnter = function() {
this.attr('x', function(d, i) {
return chart.x(i) - 0.5;
})
.attr('y', function(d) {
return chart.h - chart.margins.bottom -
chart.margins.top - chart.y(chart.datamax - d.value) - 0.5;
})
.attr('val', function(d) {
return d.value;
})
.attr('width', chart.bar_width)
.attr('height', function(d) {
return chart.y(chart.datamax - d.value);
});
};
chart.layer('bars').on('enter', onEnter);
chart.layer('bars').on('update', onEnter);
},
// return or set the max of the data. otherwise
// it will use the data max.
max : function(datamax) {
if (!arguments.length) {
return this.usermax;
}
this.usermax = datamax;
if (this.data) this.draw(this.data);
return this;
},
yFormat: function(format) {
if (!arguments.length) {
return this._yformat;
}
this._yformat = format;
return this;
},
width : function(newWidth) {
if (!arguments.length) {
return this.w;
}
// save new width
this.w = newWidth;
// adjust the x scale range
this.x.range([this.margins.left, this.w - this.margins.right]);
// adjust the base width
this.base.attr('width', this.w);
this.trigger("change:width");
if (this.data) this.draw(this.data);
return this;
},
height : function(newHeight) {
if (!arguments.length) {
return this.h;
}
// save new height
this.h = newHeight;
// adjust the y scale
this.y.range([this.h - this.margins.top, this.margins.bottom]);
// adjust the base width
this.base.attr('height', this.h);
this.trigger("change:height");
if (this.data) this.draw(this.data);
return this;
}
});
var barchart = d3.select('#vis')
.append('svg')
.chart('BarChart') //**Moving transform position out of here**
.yFormat(d3.format("d"))
.height(400)
.width(800);
var data = [
{ month : 'January', temperature : 29 },
{ month : 'February', temperature : 32 },
{ month : 'March', temperature : 48 },
{ month : 'April', temperature : 49 },
{ month : 'May', temperature : 58 },
{ month : 'June', temperature : 68 },
{ month : 'July', temperature : 74 },
{ month : 'August', temperature : 73 },
{ month : 'September', temperature : 65 },
{ month : 'October', temperature : 54 },
{ month : 'November', temperature : 45 },
{ month : 'December', temperature : 35 }
];
//**Moving transform function out of barchart definition
//so it is called only once and not everytime
//the chart is redrawn**
function transform(data) {
return data.map(function(d) {
return { name : d.month, value : d.temperature };
});
}
barchart.draw(transform(data));
In this fiddle, I am trying to display the y-axis values in percentages. Not exactly calculating the percentage from the data, but just appending the "%" sign to the existing data. I am fetching the data through an array in the actual code, it is dynamic. I have tried appending using tick format but somehow it doesn't seem to work. Any help?

Do this to append % to the y tick:
var yAxis = d3.svg.axis()
.scale(chart.y)
.orient('left')
.ticks(5)
.tickFormat(function(d){return d+ "%"});
working code here

Related

D3 only render to certain depth

For d3, given an array of nested objects.
Is it possible to only render a certain amount of depth?
I am basing off of some sunburst examples online like :
https://github.com/Nikhilkoneru/react-d3-zoomable-sunburst/blob/master/src/index.js
Using the .selectAll method from d3, can I only limit the sunburst to render 'X' depths, instead of rendering the entire array of nested objects?
I'm trying to render a really large array, and it causes a really laggy experience.
This is the selectAll that I'm using from the github example.
svg.selectAll('path')
.data(partition(root)
.descendants())
.enter()
.append('path')
.style('fill', (d) => {
let hue;
const current = d;
if (current.depth === 0) {
return '#33cccc';
}
if (current.depth <= 1) {
hue = hueDXScale(d.x0);
current.fill = d3.hsl(hue, 0.5, 0.6);
return current.fill;
}
if(current.depth <= 2){
console.log("depth > 2: ", d)
current.fill = current.parent.fill.brighter(0.5);
const hsl = d3.hsl(current.fill);
hue = hueDXScale(current.x0);
const colorshift = hsl.h + (hue / 4);
return d3.hsl(colorshift, hsl.s, hsl.l);
}
})
.attr('stroke', '#fff')
.attr('stroke-width', '1')
.on('click', d => click(d, node, svg, x, y, radius, arc))
.on('mouseover', function (d) {
if (props.tooltip) {
d3.select(this).style('cursor', 'pointer');
tooltip.html(() => {
const name = utils.formatNameTooltip(d);
return name;
});
return tooltip.transition().duration(50).style('opacity', 1);
}
return null;
})
.on('mousemove', () => {
if (props.tooltip) {
tooltip
.style('top', `${d3.event.pageY - 50}px`)
.style('left', `${props.tooltipPosition === 'right' ? d3.event.pageX - 100 : d3.event.pageX - 50}px`);
}
return null;
})
.on('mouseout', function () {
if (props.tooltip) {
d3.select(this).style('cursor', 'default');
tooltip.transition().duration(50).style('opacity', 0);
}
return null;
});
As you can see, from the current.depth, i'm able to filter out what depth is more than 2.
Is it possible to add display: none to anything more than depth of 2, or is there a better way to not render anything more than depth of 2 from current

d3js prevent forceSimulation from recalculating position all the time

I'm trying to build a force layout that will allow me to visualize the flow of objects in a system.
I want to show how many objects are on a specific state and when a state change I want to update my graph.
I've built a prototype, but I've noticed that D3.js is recalculating transform of each node even when they don't need to move:
can this be fixed? maybe there is an option to add minimum value for an update?
I've declared force layout this way:
const force = d3.forceSimulation()
.force('link', d3.forceLink().id((d) => d.id).distance(150))
.force('charge', d3.forceManyBody().strength(-500))
.force('x', d3.forceX(width / 2))
.force('y', d3.forceY(height / 2))
.on('tick', tick);
After changing alphaTarget to alpha recalculation stopped, but I got another bug:
I've added drag functionality and it stopped working with above changes.
Here is the version with fixed recalculation but with drag problem.
The culprit is in your restart() function:
force.alphaTarget(0.3).restart();
The way you are reheating your simulation by setting .alphaTarget(0.3) is not correct. alphaTarget is a configuration parameter which controls the way alpha decreases. Figuratively speaking, alpha—for as long as it is greater than alphaMin— is headed towards alphaTarget. The heat in the system is measured by alpha which can be thought of as dynamic data; alphaTarget, on the other hand, resembles more or less static data.
Furthermore, it is important to have alphaTarget set to a value less than alphaMin or else your simulation is going to run indefinitely because alpha, while on its way towards alphaTarget so to speak, is never going to be less than alphaMin.
Thus, if you want to reheat your system, you have to manipulate alpha instead of alphaTarget. Changing above mentioned line to the following is all it takes to get the desired effect.
force.alpha(0.3).restart();
Have a look at the following snippet, which is basically a fork of your JSFiddle, to see it in action.
document.getElementById("a").addEventListener("click", function() {
AddNewLink(null, 1);
});
document.getElementById("b").addEventListener("click", function() {
AddNewLink(1, 2);
});
document.getElementById("c").addEventListener("click", function() {
AddNewLink(2, 3);
});
document.getElementById("d").addEventListener("click", function() {
AddNewLink(1, 3);
});
document.getElementById("e").addEventListener("click", function() {
AddNewLink(3, 4);
});
document.getElementById("f").addEventListener("click", function() {
AddNewLink(4, 5);
});
function AddNewLink(from, to) {
var startNode;
var start = availableNodes.find(x => x.id === from);
if (start !== undefined) {
//check if this node is already added
var foundStart = nodes.find(x => x.id == start.id);
if (foundStart === undefined) {
nodes.push(start);
startNode = start;
} else {
foundStart.value--;
if (foundStart.value < 0) foundStart.value = 0;
startNode = foundStart;
}
}
var endNode;
var end = availableNodes.find(x => x.id === to);
if (end !== undefined) {
//check if this node is already added
var foundEnd = nodes.find(x => x.id == end.id);
if (foundEnd === undefined) {
nodes.push(end);
endNode = end;
end.value++;
} else {
foundEnd.value++;
endNode = foundEnd;
}
}
//console.log(startNode, endNode);
if (startNode !== undefined && endNode !== undefined) {
links.push({
source: startNode,
target: endNode
});
}
restart();
}
// set up SVG for D3
const width = 400;
const height = 400;
const colors = d3.scaleOrdinal(d3.schemeCategory10);
const svg = d3.select('svg')
.on('contextmenu', () => {
d3.event.preventDefault();
})
.attr('width', width)
.attr('height', height);
var availableNodes = [{
id: 1,
name: "Start",
value: 0,
reflexive: false
}, {
id: 2,
name: "Node 1",
value: 0,
reflexive: false
}, {
id: 3,
name: "Node 2",
value: 0,
reflexive: false
}, {
id: 4,
name: "Node 3",
value: 0,
reflexive: false
}, {
id: 5,
name: "Finish",
value: 0,
reflexive: false
}];
// set up initial nodes and links
// - nodes are known by 'id', not by index in array.
// - reflexive edges are indicated on the node (as a bold black circle).
// - links are always source < target; edge directions are set by 'left' and 'right'.
let nodes = [
availableNodes[0], availableNodes[1], availableNodes[2]
];
let links = [{
source: nodes[0],
target: nodes[1]
},
{
source: nodes[1],
target: nodes[2]
}
];
// init D3 force layout
const force = d3.forceSimulation()
.force('link', d3.forceLink().id((d) => d.id).distance(150))
.force('charge', d3.forceManyBody().strength(-500))
.force('x', d3.forceX(width / 2))
.force('y', d3.forceY(height / 2))
.on('tick', tick);
// define arrow markers for graph links
svg.append('svg:defs').append('svg:marker')
.attr('id', 'end-arrow')
.attr('viewBox', '0 -5 10 10')
.attr('refX', 8)
.attr('markerWidth', 3)
.attr('markerHeight', 3)
.attr('orient', 'auto')
.append('svg:path')
.attr('d', 'M0,-5L10,0L0,5')
.attr('fill', '#000');
// handles to link and node element groups
let path = svg.append('svg:g').attr('id', 'lines').selectAll('path');
let circle = svg.append('svg:g').attr('id', 'circles').selectAll('g');
// update force layout (called automatically each iteration)
function tick() {
// draw directed edges with proper padding from node centers
path.attr('d', (d) => {
const deltaX = d.target.x - d.source.x;
const deltaY = d.target.y - d.source.y;
const dist = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
const normX = deltaX / dist;
const normY = deltaY / dist;
const sourcePadding = d.left ? 17 : 12;
const targetPadding = d.right ? 17 : 12;
const sourceX = d.source.x + (sourcePadding * normX);
const sourceY = d.source.y + (sourcePadding * normY);
const targetX = d.target.x - (targetPadding * normX);
const targetY = d.target.y - (targetPadding * normY);
return `M${sourceX},${sourceY}L${targetX},${targetY}`;
});
circle.attr('transform', (d) => `translate(${d.x},${d.y})`);
}
// update graph (called when needed)
function restart() {
// path (link) group
path = path.data(links);
// remove old links
path.exit().remove();
// add new links
path = path.enter().append('svg:path')
.attr('class', 'link')
.style('marker-end', 'url(#end-arrow)')
.merge(path);
// circle (node) group
// NB: the function arg is crucial here! nodes are known by id, not by index!
circle = circle.data(nodes, (d) => d.id);
// update existing nodes (reflexive & selected visual states)
circle.selectAll('circle')
.style('fill', (d) => colors(d.id))
.classed('reflexive', (d) => d.reflexive);
circle.selectAll('text.value').text((d) => d.value);
// remove old nodes
circle.exit().remove();
// add new nodes
const g = circle.enter().append('svg:g');
g.append('svg:circle')
.attr('class', 'node')
.attr('r', 12)
.style('fill', (d) => colors(d.id))
.style('stroke', (d) => d3.rgb(colors(d.id)).darker().toString())
.classed('reflexive', (d) => d.reflexive)
// show node IDs
g.append('svg:text')
.attr('x', 0)
.attr('y', 4)
.attr('class', 'value')
.text((d) => d.value);
g.append('svg:text')
.attr('x', 20)
.attr('y', 4)
.attr('class', 'name')
.text((d) => d.name);
circle = g.merge(circle);
// set the graph in motion
force
.nodes(nodes)
.force('link').links(links);
force.alpha(0.3).restart();
}
restart();
svg {
background-color: #FFF;
cursor: default;
user-select: none;
}
path.link {
fill: none;
stroke: #000;
stroke-width: 3px;
cursor: default;
}
path.link.selected {
stroke-dasharray: 10, 2;
}
path.link.dragline {
pointer-events: none;
}
path.link.hidden {
stroke-width: 0;
}
circle.node.reflexive {
stroke: #000 !important;
stroke-width: 2.5px;
}
text {
font: 12px sans-serif;
pointer-events: none;
}
text.value {
text-anchor: middle;
font-weight: bold;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.9.1/d3.js"></script>
<button id='a'>1</button>
<button id='b'>1>2</button>
<button id='c'>2>3</button>
<button id='d'>1>3</button>
<button id='e'>3>4</button>
<button id='f'>4>5</button>
<svg width="400" height="400"></svg>

D3 Animate One Path Series At a Time

Learning Javascript and D3.
Trying to create a graph where it draws each line in a series, one at a time or on a delayed interval.
What I've got so far (relevant code at the end of this post)
https://jsfiddle.net/jimdholland/5n6xrLk0/180/
My data is structures with each series as one row, with 12 columns. When you read it in, the object approximately looks like
mydata = [
{NumDays01: "0", NumDays02: "0", NumDays03: "0", NumDays04: "0", NumDays05: "0",Numdays06: 30}
1: {NumDays01: "0", NumDays02: "0", NumDays03: "0", NumDays04: "0",...
]
I can get it to create line, but it draws all the paths at once. Likewise, I've tried a loop, but that still draws them all at once. Also tried d3.timer and similar results. But I'm having a hard time understanding the timer callback stuff and creating those functions correctly.
I haven't found another example to really study other than a chart from FlowingData which has a lot of bells and whistles out of my league.
https://flowingdata.com/2019/02/01/how-many-kids-we-have-and-when-we-have-them/
Any help or tips would be appreciated.
var svg = d3.select("#chart").select("svg"),
margin = { top: 30, right: 20, bottom: 10, left: 40 },
width = parseInt(d3.select("#chart").style('width'), 10) - margin.left - margin.right;
d3.tsv("https://s3.us-east-2.amazonaws.com/jamesdhollandwebfiles/data/improvementTest.tsv", function(error, data) {
if (error) throw error;
console.log(data);
myData = data;
var t = d3.timer(pathMaker);
}); // #end d3.tsv()
function pathMaker() {
var peeps = myData[rowToRun];
var coords = [];
var lineToRemove = [];
for (var nameIter in dayList) {
coords.push({ x: x(nameIter), y: y(peeps[dayList[nameIter]])})
}
var improvementPath = g.append("path")
.datum(coords)
.attr("id", "path" + peeps.caseid)
.attr("d", lineMaker)
.attr("stroke", "#90c6e4")
.attr("fill", "none")
.attr("stroke-width", 2);
var total_length = improvementPath.node().getTotalLength();
var startPoint = pathStartPoint(improvementPath);
improvementPath = improvementPath
.attr("stroke-dasharray", total_length + " " + total_length)
.attr("stroke-dashoffset", total_length)
.transition() // Call Transition Method
.duration(4000) // Set Duration timing (ms)
.ease(d3.easeLinear) // Set Easing option
.attr("stroke-dashoffset", 0); // Set final value of dash-offset for transition
rowToRun += 1;
if (rowToRun == 5) {rowToRun = 0;}
}
//Get path start point for placing marker
function pathStartPoint(Mypath) {
var d = Mypath.attr("d"),
dsplitted = d.split(/M|L/)[1];
return dsplitted;
}
there are a few problems. I tried to configure your code as little as I could to make it work. If you need further explanations, please let me know
d3.tsv("https://s3.us-east-2.amazonaws.com/jamesdhollandwebfiles/data/improvementTest.tsv", function(error, data) {
if (error) throw error;
console.log(data);
myData = data;
pathMaker()
}); // #end d3.tsv()
function pathMaker() {
var peeps = myData[rowToRun];
var coords_data = [];
var lineToRemove = [];
for (let i = 0; i < myData.length; i++) {
var coords = [];
for (var nameIter in dayList) {
coords.push({ x: x(nameIter), y: y(myData[i][dayList[nameIter]])})
}
coords_data.push(coords)
}
console.log(coords_data)
var improvementPath = g.selectAll("path")
.data(coords_data)
.enter()
.append("path")
.attr("d", lineMaker)
.attr("stroke", "#90c6e4")
.attr("fill", "none")
.attr("stroke-width", 2);
improvementPath = improvementPath.each(function (d,i) {
var total_length = this.getTotalLength();
var startPoint = pathStartPoint(improvementPath);
const path = d3.select(this)
.attr("stroke-dasharray", total_length + " " + total_length)
.attr("stroke-dashoffset", total_length)
.transition() // Call Transition Method
.duration(4000) // Set Duration timing (ms)
.delay(i*4000)
.ease(d3.easeLinear) // Set Easing option
.attr("stroke-dashoffset", 0); // Set final value of dash-offset for transition
})
rowToRun += 1;
if (rowToRun == 5) {rowToRun = 0;}
}

animate grow and radius at the same time

I made a simple animated pie/donut chart with d3 and I was wondering if it would be possible to animate the radius and the grow at the same time.
As you can see from the example or the snippet below, only the grow is animated.
const dataset = {
apples: [{
label: 'Category A',
value: 53245,
isSelected: true
}, {
label: 'Category B',
value: 28479,
isSelected: false
}, {
label: 'Category C',
value: 24037,
isSelected: false
}, {
label: 'Category D',
value: 40245,
isSelected: false
}, {
label: 'Category E',
value: 30245,
isSelected: false
}],
oranges: [{
label: 'Category A',
value: 200,
isSelected: false
}, {
label: 'Category B',
value: 200,
isSelected: true
}, {
label: 'Category C',
value: 200,
isSelected: false
}, {
label: 'Category D',
value: 200,
isSelected: false
}]
};
/**
* Pie chart class
*/
function PieChart(options) {
// Observable stream source
this.selectionSource = new Rx.Subject();
// Observable stream
this.selection = this.selectionSource.asObservable();
// Chart options/settings
this.width = options.width;
this.height = options.height;
this.radius = Math.min(this.width, this.height) / 2;
this.multiple = options.multiple;
this.legend = options.legend;
this.colorRange = d3.scale.category20();
this.color = d3.scale.ordinal()
.range(this.colorRange.range());
// Animation directions
this.clockwise = {
startAngle: 0,
endAngle: 0
};
this.counterclock = {
startAngle: Math.PI * 2,
endAngle: Math.PI * 2
};
// Create the SVG on which the plot is painted.
this.svg = d3.select(options.target)
.append('svg:svg')
.attr('width', this.width)
.attr('height', this.height)
.append('g')
.attr('transform', `translate(${this.width / 2}, ${this.height / 2})`);
// Initial path creation.
this.path = this.svg.selectAll('path');
// Create the pie layout.
this.pie = d3.layout.pie()
.value(function(d) {
return d.value;
})
.sort(null);
// Create arc functions.
this.arc = d3.svg.arc()
.innerRadius(this.radius - 100)
.outerRadius(this.radius - 20);
// Arc when a slice is selected/toggled on.
this.arcSelected = d3.svg.arc()
.innerRadius(this.radius - 90)
.outerRadius(this.radius - 10);
this.arcTween = arcTween;
this.arcTweenOut = arcTweenOut;
this.updateSelection = updateSelection;
// Used by some of the functions that get a different context when called by d3.
const thisRef = this;
// Store the displayed angles in `current`.
// Then, interpolate from `current` to the new angles.
// During the transition, `current` is updated in-place by d3.interpolate.
function arcTween(a) {
const i = d3.interpolate(this.current, a);
this.current = i(0);
const slice = d3.select(this);
return arcFn(slice, i);
}
function arcTweenOut() {
const i = d3.interpolate(this.current, {
startAngle: Math.PI * 2,
endAngle: Math.PI * 2,
value: 0
});
this.current = i(0);
const slice = d3.select(this);
return arcFn(slice, i);
}
function arcFn(slice, i) {
return function(t) {
if (slice.classed('selected')) {
return thisRef.arcSelected(i(t));
}
return thisRef.arc(i(t));
};
}
// NOTE: `this` will not be the class context,
// but the contaxt set
function updateSelection(d) {
const node = this;
const slice = d3.select(node);
const isToggled = slice.classed('selected');
const event = {
data: d.data
};
if (thisRef.multiple) {
// Allow multiple slice toggling.
toggle();
} else {
// Find previously selected slice.
const selected = thisRef.svg.selectAll('path')
.filter(function() {
return !this.isEqualNode(node) && d3.select(this).classed('selected');
});
// Deselect previous selection.
if (!selected.empty()) {
selected.classed('selected', false)
.transition()
.attr('d', thisRef.arc);
}
// Toggle current slice.
toggle();
}
function toggle() {
if (isToggled) {
event.selected = false;
slice.classed('selected', false)
.transition()
.attr('d', thisRef.arc)
.each('end', emit);
} else {
event.selected = true;
slice.classed('selected', true)
.transition()
.attr('d', thisRef.arcSelected)
.each('end', emit);
}
}
function emit() {
thisRef.selectionSource.onNext(event);
}
}
}
PieChart.prototype.direction = function direction() {
// Set the start and end angles to Math.PI * 2 so we can transition counterclockwise to the actual values later.
let direction = this.counterclock;
// Set the start and end angles to 0 so we can transition clockwise to the actual values later.
if (!this.painted) {
direction = this.clockwise;
}
return direction;
}
PieChart.prototype.update = function update(data) {
const direction = this.direction();
const thisRef = this;
this.path = this.path
.data(this.pie(data), function(d) {
return d.data.label;
})
.classed('selected', selected.bind(this));
function selected(datum) {
return datum.data.isSelected;
}
// Append slices when data is added.
this.path.enter()
.append('svg:path')
.attr('class', 'slice')
.style('stroke', '#f3f5f6')
.attr('stroke-width', 2)
.attr('fill', function(d, i) {
return thisRef.color(d.data.label);
})
.attr('d', this.arc(direction))
// Store the initial values.
.each(function(d) {
this.current = {
data: d.data,
value: d.value,
startAngle: direction.startAngle,
endAngle: direction.endAngle
};
})
.on('click', this.updateSelection);
// Remove slices when data is removed.
this.path.exit()
.transition()
.duration(450)
.attrTween('d', this.arcTweenOut)
// Now remove the exiting arcs.
.remove();
// Redraw the arcs.
this.path.transition()
.duration(450)
.attrTween('d', this.arcTween);
// Add legend
this.addLegend();
// Everything is painted now,
// we only do updates from this point on.
if (!this.painted) {
this.painted = true;
}
}
PieChart.prototype.addLegend = function addLegend() {
// The legend does not need to be repainted when we update the slices.
if (this.painted || !this.legend) {
return;
}
const thisRef = this;
const rect = this.radius * 0.04;
const spacing = this.radius * 0.02;
const legend = this.svg.selectAll('.legend')
.data(this.color.domain());
legend.enter()
.append('g')
.attr('class', 'legend')
.attr('fill-opacity', 0)
.attr('transform', function(d, i) {
const height = rect + spacing * 2;
const offset = height * thisRef.color.domain().length / 2;
const horizontal = -4 * rect;
const vertical = i * height - offset;
return `translate(${horizontal}, ${vertical})`;
});
legend.append('rect')
.attr('width', rect)
.attr('height', rect)
.style('fill', this.color);
legend.append('text')
.attr('x', rect + spacing)
.attr('y', rect)
.text(function(d) {
return d;
});
legend.transition()
.duration(450)
.attr('fill-opacity', 1);
};
// DEMO/USAGE
const pieChart = new PieChart({
target: '#chart',
multiple: true,
legend: true,
width: 400,
height: 400
});
console.log(pieChart);
pieChart.selection.subscribe(function(selection) {
console.log(selection);
});
// Paint the plot.
pieChart.update(dataset.apples);
// This is only here for demo purposes
d3.selectAll("input")
.on("change", update);
var timeout = setTimeout(function() {
d3.select("input[value=\"oranges\"]").property("checked", true).each(update);
}, 2000);
function update() {
clearTimeout(timeout); // This is only here for demo purposes
// Update the data.
pieChart.update(dataset[this.value]);
}
body {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
}
.container {
position: relative;
}
form {
position: absolute;
right: 10px;
top: 10px;
}
// Graph
.slice {
cursor: pointer;
}
.legend {
font-size: 12px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/4.1.0/rx.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<div class="container">
<form>
<label>
<input type="radio" name="dataset" value="apples" checked> Apples</label>
<label>
<input type="radio" name="dataset" value="oranges"> Oranges</label>
</form>
<div id="chart"></div>
</div>
Here is a jsfiddle example showing how you can achieve that: https://jsfiddle.net/kmandov/9jrb1qLr/
I've used Mike Bostock's pie chart example as a base, but you can adapt the code to your PieChart implementation.
The basic idea is that as soon as you switch category(oranges/apples), the pie arcs are recalculated to match the new data. The animation is done via a transition in the change function:
function change() {
// ... calculate arcs
path.transition().duration(750).attrTween("d", arcTween(selected));
}
then the real magic is happening in the arcTween function. In the original example only the start and end angles are updated. You can store the target outerRadius and then update the arc generator on each step of the transition:
function arcTween(selected) {
return function(target, i) {
target.outerRadius = radius - (i === selected ? 0 : 20);
var arcInterpolator = d3.interpolate(this._current, target);
this._current = arcInterpolator(0);
return function(t) {
var interpolatedArc = arcInterpolator(t);
arc.outerRadius(interpolatedArc.outerRadius);
return arc(interpolatedArc);
};
}
}

D3 Candlestick chart date on xaxis

I am trying to create candlestick chart using d3.js, it works great but I cannot solve a problem. I am sure it is covered somewhere but I cannot find it.
I have candlestick data, here is one record:
{
"Date":"02/01/2010",
"Time":1265004000,
"YDay":31,
"Open":1.571500,
"High":1.582100,
"Low":1.558700,
"Close":1.580500,
"SMA":1.5663,
"EMA":1.56743786563595
},
As you can see, the real xaxis is the time value which is epoch time (# of seconds since 1/1/70). I need it working with those seconds to properly format the chart, but I want the x axis labeled with "Date" value instead of the seconds. With this code the plot is great but the x axis is just a long set of numbers (the # of seconds overwriting each other).
How can I get it to do the calculations in seconds, but display the dates on the x axis.
Here is a self contained html file with embedded data. Just cut and paste into index.html on your disk and double click.
<html>
<script src="http://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script>
var xs;
var ys;
var base_width = 960;
var base_height = 600;
var x_min;
var x_max;
var y_min;
var y_max;
var barWidth;
var candleWidth;
var xAxis;
var yAxis;
var margin = {top: 20, right: 30, bottom: 30, left: 40};
function getYLow (d)
{
return ys (d.Low);
}
function getYHigh (d)
{
return ys (d.High);
}
function getYOC (d)
{
var yOpen = ys (d.Open);
var yClose = ys (d.Close);
if (yOpen < yClose)
{
return yOpen;
}
return yClose;
}
function getYHeight (d)
{
var yHeight = ys (d.Open) - ys (d.Close);
if (yHeight < 0)
{
yHeight *= -1;
}
return yHeight;
}
function getX (d)
{
// offset by half the width of my candlestick rectangle
// but centered on the actual time
var x = xs (d.Time) - (candleWidth / 2.0);
return x;
}
function getXActual (d)
{
var x = xs (d.Time);
return x;
}
function getColor (d)
{
// I want the candlestick to be green if it closed higher
// then the open otherwise it should be red
if (d.Close > d.Open)
return "green";
return "red";
}
function load ()
{
// load the data and create the chart
width = base_width - margin.left - margin.right,
height = base_height - margin.top - margin.bottom;
ys = d3.scale.linear ().range ([height, 0]);
xs = d3.scale.linear ().range ([0, width]);
var svg = d3.select ("body").append ("svg")
.attr ("width", width + margin.left + margin.right)
.attr ("height", height + margin.top + margin.bottom)
.style ("background-color", "black")
.append ("g")
.attr ("transform", "translate(" + margin.left + "," + margin.top + ")");
xAxis = d3.svg.axis()
.scale(xs)
.orient("bottom");
yAxis = d3.svg.axis()
.scale(ys)
.orient("left");
y_min = d3.min (data, function (d) { return d.Low; });
y_max = d3.max (data, function (d) { return d.High; });
x_min = data [0].Time;
x_max = data [data.length - 1].Time;
// offset min and max by 1 day to give some margin so the candlestick
// rectangle stays on the chart and does not cross the axis
x_min = +x_min - 86400;
x_max = +x_max + 86400;
ys.domain ([y_min, y_max]);
xs.domain ([x_min, x_max]);
// this is the actual width of 1 day (1 bar)
barWidth = xs (+x_min + 86400) - xs (+x_min);
// the candle should be 75% the width of the day
candleWidth = barWidth * 0.75;
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.attr("stroke", "white")
.call(xAxis);
svg.append("g")
.attr("class", "y axis")
.attr("stroke", "white")
.call(yAxis);
svg.selectAll (".candle_bar")
.data (data)
.enter ().append ("rect")
.attr ("class", "candle_bar")
.attr ("x", getX)
.attr ("y", getYOC)
.attr ("height", getYHeight)
.attr ("width", candleWidth)
.attr ("fill", getColor);
svg.selectAll ("candle_hl")
.data (data)
.enter ().append ("svg:line")
.attr ("x1", getXActual)
.attr ("y1", getYLow)
.attr ("x2", getXActual)
.attr ("y2", getYHigh)
.style ("stroke", getColor);
// for plotting an sma and ema line
var line_sma = d3.svg.line ().x (function (d, i) { return xs (d.YDay); })
.y (function (d, i) { return ys (d.SMA); });
svg.append ("path")
.attr ("d", line_sma (data))
.attr ("stroke", "white")
.attr ("fill", "none");
var line_ema = d3.svg.line ().x (function (d, i) { return xs (d.YDay); })
.y (function (d, i) { return ys (d.EMA); });
svg.append ("path")
.attr ("d", line_ema (data))
.attr ("stroke", "orange")
.attr ("fill", "none");
}
function type (d)
{
d.Time = +d.Time;
d.Open = +d.Open;
d.High = +d.High;
d.Low = +d.Low;
d.Close = +d.Close;
d.SMA = +d.SMA;
d.EMA = +d.EMA;
return d;
}
$( document ).ready (load);
var data = [
{
"Date":"02/01/2010",
"Time":1265004000,
"YDay":31,
"Open":1.571500,
"High":1.582100,
"Low":1.558700,
"Close":1.580500,
"SMA":1.5663,
"EMA":1.56743786563595
},
{
"Date":"02/02/2010",
"Time":1265090400,
"YDay":32,
"Open":1.580500,
"High":1.586100,
"Low":1.572600,
"Close":1.576900,
"SMA":1.56758,
"EMA":1.56933029250876
},
{
"Date":"02/03/2010",
"Time":1265176800,
"YDay":33,
"Open":1.576700,
"High":1.581900,
"Low":1.571300,
"Close":1.576900,
"SMA":1.56832,
"EMA":1.57084423400701
},
{
"Date":"02/04/2010",
"Time":1265263200,
"YDay":34,
"Open":1.576900,
"High":1.595000,
"Low":1.571000,
"Close":1.580600,
"SMA":1.57017,
"EMA":1.5727953872056
},
{
"Date":"02/05/2010",
"Time":1265349600,
"YDay":35,
"Open":1.580300,
"High":1.586300,
"Low":1.573100,
"Close":1.574500,
"SMA":1.57075,
"EMA":1.57313630976448
},
{
"Date":"02/07/2010",
"Time":1265522400,
"YDay":37,
"Open":1.574300,
"High":1.576800,
"Low":1.571400,
"Close":1.575100,
"SMA":1.57224,
"EMA":1.57352904781159
},
{
"Date":"02/08/2010",
"Time":1265608800,
"YDay":38,
"Open":1.575000,
"High":1.583500,
"Low":1.572700,
"Close":1.577500,
"SMA":1.5745,
"EMA":1.57432323824927
},
{
"Date":"02/09/2010",
"Time":1265695200,
"YDay":39,
"Open":1.577400,
"High":1.578200,
"Low":1.567100,
"Close":1.571400,
"SMA":1.5753,
"EMA":1.57373859059942
},
{
"Date":"02/10/2010",
"Time":1265781600,
"YDay":40,
"Open":1.571300,
"High":1.573800,
"Low":1.549800,
"Close":1.551600,
"SMA":1.57365,
"EMA":1.56931087247953
},
{
"Date":"02/11/2010",
"Time":1265868000,
"YDay":41,
"Open":1.551700,
"High":1.552800,
"Low":1.533500,
"Close":1.537500,
"SMA":1.57025,
"EMA":1.56294869798363
},
{
"Date":"02/12/2010",
"Time":1265954400,
"YDay":42,
"Open":1.537500,
"High":1.545000,
"Low":1.526900,
"Close":1.535200,
"SMA":1.56572,
"EMA":1.5573989583869
},
{
"Date":"02/14/2010",
"Time":1266127200,
"YDay":44,
"Open":1.533200,
"High":1.536100,
"Low":1.531800,
"Close":1.533100,
"SMA":1.56134,
"EMA":1.55253916670952
},
{
"Date":"02/15/2010",
"Time":1266213600,
"YDay":45,
"Open":1.533200,
"High":1.535300,
"Low":1.525500,
"Close":1.526500,
"SMA":1.5563,
"EMA":1.54733133336762
},
{
"Date":"02/16/2010",
"Time":1266300000,
"YDay":46,
"Open":1.526500,
"High":1.529100,
"Low":1.519400,
"Close":1.528000,
"SMA":1.55104,
"EMA":1.54346506669409
},
{
"Date":"02/17/2010",
"Time":1266386400,
"YDay":47,
"Open":1.527900,
"High":1.528400,
"Low":1.511300,
"Close":1.515100,
"SMA":1.5451,
"EMA":1.53779205335528
},
{
"Date":"02/18/2010",
"Time":1266472800,
"YDay":48,
"Open":1.515000,
"High":1.516100,
"Low":1.506300,
"Close":1.513100,
"SMA":1.5389,
"EMA":1.53285364268422
},
{
"Date":"02/19/2010",
"Time":1266559200,
"YDay":49,
"Open":1.513100,
"High":1.516900,
"Low":1.509500,
"Close":1.514100,
"SMA":1.53256,
"EMA":1.52910291414738
},
{
"Date":"02/21/2010",
"Time":1266732000,
"YDay":51,
"Open":1.512400,
"High":1.514200,
"Low":1.510900,
"Close":1.513200,
"SMA":1.52674,
"EMA":1.5259223313179
},
{
"Date":"02/22/2010",
"Time":1266818400,
"YDay":52,
"Open":1.513200,
"High":1.515600,
"Low":1.508800,
"Close":1.511900,
"SMA":1.52277,
"EMA":1.52311786505432
},
{
"Date":"02/23/2010",
"Time":1266904800,
"YDay":53,
"Open":1.511700,
"High":1.522000,
"Low":1.504000,
"Close":1.515900,
"SMA":1.52061,
"EMA":1.52167429204346
},
{
"Date":"02/24/2010",
"Time":1266991200,
"YDay":54,
"Open":1.515900,
"High":1.526600,
"Low":1.511100,
"Close":1.516400,
"SMA":1.51873,
"EMA":1.52061943363477
},
{
"Date":"02/25/2010",
"Time":1267077600,
"YDay":55,
"Open":1.516200,
"High":1.532300,
"Low":1.514400,
"Close":1.526500,
"SMA":1.51807,
"EMA":1.52179554690781
},
{
"Date":"02/26/2010",
"Time":1267164000,
"YDay":56,
"Open":1.526400,
"High":1.529200,
"Low":1.517800,
"Close":1.522200,
"SMA":1.51764,
"EMA":1.52187643752625
},
{
"Date":"02/28/2010",
"Time":1267336800,
"YDay":58,
"Open":1.519800,
"High":1.520400,
"Low":1.514200,
"Close":1.515600,
"SMA":1.5164,
"EMA":1.520621150021
}
];
</script>
<body>
</body>
</html>
Oleg, was correct I can use Time Scales, I had to convert my times from seconds to miliseconds and it work. I now have a different problem with axis but I will start another question rather then chain on this one.
Thank you Oleg the part I was missing when I tried it was the milliseconds versus seconds.

Categories