Place several symbols on top of each candle on the canvas chart - d3.js

Created a simplified example of what I need. There are 50 items in the series. Each item has a vertical list of five + symbols.
const data = [...Array(50).keys()];
const container = document.querySelector('d3fc-canvas');
const xScale = d3.scaleLinear().domain([0, data.length - 1]);
const yScale = d3.scaleLinear().domain(fc.extentLinear()(data));
const series = fc
.seriesCanvasPoint()
.xScale(xScale)
.yScale(yScale)
.crossValue((_, i) => i)
.mainValue(d => d)
.size((_, i) => 0)
.decorate((context, datum) => {
// Draw 5 symbols above current value
for (let i = 0; i < 5; i++) {
const y = yScale.invert(datum + i)
context.textAlign = 'center';
context.fillStyle = '#000';
context.font = '25px Arial';
context.fillText('+', 0, 0);
context.translate(0, y);
}
});
d3.select(container)
.on('draw', () => {
series(data);
})
.on('measure', () => {
const {
width,
height
} = event.detail;
xScale.range([0, width]);
yScale.range([height, 0]);
const ctx = container.querySelector('canvas').getContext('2d');
series.context(ctx);
});
container.requestRedraw();
body {
color: #1b1e23;
font-size: small;
font-family: sans-serif;
height: calc(100vh - 2em);
margin: 1em;
display: flex;
}
body>* {
flex: auto;
}
.domain,
.gridline-y,
.gridline-x,
.annotation-line>line {
stroke: currentColor;
stroke-opacity: 0.1;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<script src="https://unpkg.com/d3fc/build/d3fc.js"></script>
<d3fc-canvas use-device-pixel-ratio></d3fc-canvas>
Now, I'm trying to make it work with an actual financial chart and place these five + symbols vertically within each candle. It doesn't work because I don't know how to correctly convert Y value of each + to the coordinate system of the canvas.
const stream = fc.randomFinancial().stream();
const data = stream.take(20);
const scaleX = d3.scalePoint();
const scaleY = d3.scaleLinear();
const yExtent = fc.extentLinear().accessors([d => d.high, d => d.low]);
const xExtent = fc.extentLinear().accessors([d => d.date.getTime()]);
const gridlines = fc.annotationCanvasGridline();
const candlestick = fc.seriesCanvasCandlestick()
.crossValue((o, i) => o.date.getTime())
.lowValue((o, i) => o.low)
.highValue((o, i) => o.high)
.openValue((o, i) => o.open)
.closeValue((o, i) => o.close);
const points = fc
.seriesCanvasPoint()
.crossValue((o, i) => o.date.getTime())
.mainValue(d => d.close)
.size((_, i) => 0)
.decorate((context, datum) => {
const arrows = getMarks(datum.open, datum.close);
//context.translate(0, 0);
// Draw 5 symbols above current value
for (let i = 0; i < arrows.length; i++) {
const posCurrent = scaleY.invert(arrows[i])
//context.translate(0, 15);
context.translate(0, posCurrent); // move to current arrow's position
context.textAlign = 'center';
context.fillStyle = '#000';
context.font = '25px Arial';
context.fillText('+', 0, 0);
context.translate(0, -posCurrent); // reset and go back to start point
}
});
const multi = fc
.seriesCanvasMulti()
.series([gridlines, candlestick, points]);
const chart = fc
.chartCartesian(scaleX, scaleY)
.canvasPlotArea(multi);
function getMarks(open, close) {
let price = close;
const arrows = [];
const gap = Math.abs(close - open) / 5;
for (let i = 0; i < 5; i++) {
arrows.push(close > open ? price -= gap : price += gap);
}
return arrows;
}
function renderChart() {
data.push(stream.next());
data.shift();
scaleX.domain(data.map(o => o.date.getTime()));
scaleY.domain(yExtent(data));
chart
.xDomain(scaleX.domain())
.yDomain(scaleY.domain());
d3.select('#chart')
.datum(data)
.call(chart);
}
renderChart();
setInterval(renderChart, 1000);
#chart {
height: 500px;
}
<script src="https://unpkg.com/d3"></script>
<script src="https://unpkg.com/d3fc"></script>
<div id="chart"></div>
Is there a way to properly show these marks on top of the candle?
Cross-reference to Github for more details and examples.
https://github.com/d3fc/d3fc/issues/1635

Firstly, scale.invert is meant for when you have the pixel coordinate, but not the corresponding datum value. This is relevant when you are doing clicks on a screen, for example. In this case, it's not the right tool for the job. You already have access to datum, so you just need to call scaleY(datum.close).
However, d3fc uses canvas.translate() to change the zero point to .mainvalue(). Therefore, you cannot simply use scaleY(datum.close) to get the y-coordinate of the close value. Instead of (x, y), each point you want to draw becomes (0, y(arrow) - y(d.close)). So you need to change the logic to reflect this.
Needing to clean up after yourself with context.translate() is a recipe for bugs, It's almost always better to avoid such things.
As a final note, I had some confusion about your getMarks function. If you want to draw 5 arrows evenly distributed over your bar, you need to have a gap of 1/4th of the bar height. Think of it like trees on a street. If the trees at 10 feet apart and the street is 100 feet long, you'll have 11 trees, not 10. Because of this, I drew 6 plus signs, not 5, because otherwise it looked kind of off balance.
const stream = fc.randomFinancial().stream();
const data = stream.take(20);
const scaleX = d3.scalePoint();
const scaleY = d3.scaleLinear();
const yExtent = fc.extentLinear().accessors([d => d.high, d => d.low]);
const xExtent = fc.extentLinear().accessors([d => d.date.getTime()]);
const gridlines = fc.annotationCanvasGridline();
const candlestick = fc.seriesCanvasCandlestick()
.crossValue((o, i) => o.date.getTime())
.lowValue((o, i) => o.low)
.highValue((o, i) => o.high)
.openValue((o, i) => o.open)
.closeValue((o, i) => o.close);
const points = fc
.seriesCanvasPoint()
.crossValue((o, i) => o.date.getTime())
.mainValue(d => d.close)
.size((_, i) => 0)
.decorate((context, datum) => {
const arrows = getMarks(datum.open, datum.close);
const top = scaleY(datum.close);
// We need a little bit of offset, because the "+" is 20 pixels high
// and we want it in the middle of the point, not above it.
const offset = 10;
// Draw 5 symbols above current value
for (let i = 0; i < arrows.length; i++) {
context.textAlign = 'center';
context.fillStyle = '#000';
context.font = '25px Arial';
context.fillText('+', 0, scaleY(arrows[i]) - top + offset);
}
});
const multi = fc
.seriesCanvasMulti()
.series([gridlines, candlestick, points]);
const chart = fc
.chartCartesian(scaleX, scaleY)
.canvasPlotArea(multi);
function getMarks(open, close) {
let price = Math.min(open, close);
const arrows = [];
const gap = Math.abs(close - open) / 5;
for (let i = 0; i < 6; i++) {
arrows.push(price + i * gap);
}
return arrows;
}
function renderChart() {
data.push(stream.next());
data.shift();
scaleX.domain(data.map(o => o.date.getTime()));
scaleY.domain(yExtent(data));
chart
.xDomain(scaleX.domain())
.yDomain(scaleY.domain());
d3.select('#chart')
.datum(data)
.call(chart);
}
renderChart();
setInterval(renderChart, 1000);
#chart {
height: 500px;
}
<script src="https://unpkg.com/d3"></script>
<script src="https://unpkg.com/d3fc"></script>
<div id="chart"></div>

Related

Crossfilter for downsampling data in dc.js plot

I'm trying to downsample an array that has ~10,000,000 points based on the current zoom state.
The downsampling mechanism is through downsample module, using LTTB function; below is a simple example of how it's done crossfilter
const ARRAY_LENGTH = 1e7;
const MAX_VAL = 40;
const randomArray = [];
for (let i = 0; i < ARRAY_LENGTH; i += 1) {
randomArray.push({ x: i, y: Math.floor(Math.random() * MAX_VAL) });
}
export { randomArray };
const chartWidth = 10000;
export const downsampledData = LTTB(randomArray, chartWidth);
export const downsampledDataArray = downsampledData as Array<{
x: number;
y: number;
}>;
Now I'm trying to figure out how to implement LTTB using crossfilter, so I can use zoom callback to only render the 10,000 points within the zoom window.
The simplified dc.js code is as follows:
const xy = crossfilter(downsampledDataArray);
const dimension = xy.dimension((d) => [d.x, d.y]);
const group = dimension.group();
const chart = dc
// eslint-disable-next-line #typescript-eslint/ban-ts-comment
// #ts-ignore
.lineChart(divRef) // #type error
.margins({ top: 10, right: 50, bottom: 50, left: 60 })
.x(d3.scaleLinear().domain([0, 1e7]))
.yAxisLabel('Photons/s')
.xAxisLabel('Time')
.keyAccessor((d) => {
return d.key[0];
})
.valueAccessor((d) => {
return d.key[1];
})
.clipPadding(10)
.renderArea(false)
.dimension(dimension)
.mouseZoomable(true)
// .excludedColor('#ddd')
.group(group);
Here's a stackblitz with chart rendering using the above pre-downsampled data

Solution for route or path marking with bends

I want to give my users an easy way to visually trace a route on a map or a picture. The solution must let the users add control points that they can use to put bends into the route.
It should work with html5 canvas - I currently use the Konvajs library so a solution that uses this would be good.
In the interests of sharing & learning, if you can suggest solutions using other HTML5 canvas libraries that would be good to see too.
Note: This is not the original question posed. However it emerged over time that this was the actual requirement. The OP asked for means to find an arbitrary point part way along a line / curve in an HTML5 canvas so that a draggable control point could be added at that point to edit the line / curve. The accepted answer does not meet this need. However, an answer to this original question would involve serious collision-detection math and potentially use of bezier control points - in other words it would be a big ask whilst the accepted answer is a very approachable solution with consistent UX.
The original question can be seen in via the edit links below this question.
How about this idea. You click where you want the next point and the route line extends with new positioning handles along the line segments. If you need arrows you can extend the objects herein as you require. You can easily change colours, stroke width, circle opacity etc with attributes of the route class. The points are available in an array and in the standard Konva.js line points list. The JS is vanilla, no other libraries needed or used.
The Export button shows how to grab the (x,y) fixed point objects for export purposes.
Example video here, working code in below snippet.
// Set up the canvas / stage
var s1 = new Konva.Stage({container: 'container1', width: 600, height: 300});
// Add a layer for line
var lineLayer = new Konva.Layer({draggable: false});
s1.add(lineLayer);
// Add a layer for drag points
var pointLayer = new Konva.Layer({draggable: false});
s1.add(pointLayer);
// Add a rectangle to layer to catch events. Make it semi-transparent
var r = new Konva.Rect({x:0, y: 0, width: 600, height: 300, fill: 'black', opacity: 0.1})
pointLayer.add(r)
// Everything is ready so draw the canvas objects set up so far.
s1.draw()
// generic canvas end
// Class for the draggable point
// Params: route = the parent object, opts = position info, doPush = should we just make it or make it AND store it
var DragPoint = function(route, opts, doPush){
var route = route;
this.x = opts.x;
this.y = opts.y;
this.fixed = opts.fixed;
this.id = randId(); // random id.
if (doPush){ // in some cases we want to create the pt then insert it in the run of the array and not always at the end
route.pts.push(this);
}
// random id generator
function randId() {
return Math.random().toString(36).replace(/[^a-z]+/g, '').substr(2, 10);
}
// mark the pt as fixed - important state, shown by filled point
this.makeFixed = function(){
this.fixed = true;
s1.find('#' + this.id)
.fill(route.fillColor);
}
this.kill = function(){
s1.find('#' + this.id)
.remove();
}
this.draw = function(){
// Add point & pt
var circleId = this.id;
var pt = new Konva.Circle({
id: circleId,
x: this.x,
y: this.y,
radius: route.pointRadius,
opacity: route.pointOpacity,
strokeWidth: 2,
stroke: route.strokeColor,
fill: 'transparent',
draggable: 'true'
})
pt.on('dragstart', function(){
route.drawState = 'dragging';
})
pt.on('dragmove', function(){
var pos = this.getPosition();
route.updatePt(this.id(), pos)
route.calc(this.id());
route.draw();
})
pt.on('dragend', function(){
route.drawState = 'drawing';
var pos = this.getPosition();
route.updatePt(this.getId(), pos);
route.splitPts(this.getId());
route.draw();
})
if (this.fixed){
this.makeFixed();
}
route.ptLayer.add(pt);
route.draw();
}
}
var Route = function() {
this.lineLayer = null;
this.ptLayer = null;
this.drawState = '';
this.fillColor = 'Gold';
this.strokeColor = 'Gold';
this.pointOpacity = 0.5;
this.pointRadius = 10;
this.color = 'LimeGreen';
this.width = 5;
this.pts = []; // array of dragging points.
this.startPt = null;
this.endPt = null;
// reset the points
this.reset = function(){
for (var i = 0; i < this.pts.length; i = i + 1){
this.pts[i].kill();
}
this.pts.length = 0;
this.draw();
}
// Add a point to the route.
this.addPt = function(pos, isFixed){
if (this.drawState === 'dragging'){ // do not add a new point because we were just dragging another
return null;
}
this.startPt = this.startPt || pos;
this.endPt = pos;
// create this new pt
var pt = new DragPoint(this, {x: this.endPt.x, y: this.endPt.y, fixed: isFixed}, true, "A");
pt.draw();
pt.makeFixed(); // always fixed for manual points
// if first point ignore the splitter process
if (this.pts.length > 0){
this.splitPts(pt.id, true);
}
this.startPt = this.endPt; // remember the last point
this.calc(); // calculate the line points from the array
this.draw(); // draw the line
}
// Position the points.
this.calc = function (draggingId){
draggingId = (typeof draggingId === 'undefined' ? '---' : draggingId); // when dragging an unfilled point we have to override its automatic positioning.
for (var i = 1; i < this.pts.length - 1; i = i + 1){
var d2 = this.pts[i];
if (!d2.fixed && d2.id !== draggingId){ // points that have been split are fixed, points that have not been split are repositioned mid way along their line segment.
var d1 = this.pts[i - 1];
var d3 = this.pts[i + 1];
var pos = this.getHalfwayPt(d1, d3);
d2.x = pos.x;
d2.y = pos.y;
}
s1.find('#' + d2.id).position({x: d2.x, y: d2.y}); // tell the shape where to go
}
}
// draw the line
this.draw = function (){
if (this.drawingLine){
this.drawingLine.remove();
}
this.drawingLine = this.newLine(); // initial line point
for (var i = 0; i < this.pts.length; i = i + 1){
this.drawingLine.points(this.drawingLine.points().concat([this.pts[i].x, this.pts[i].y]))
}
this.ptLayer.draw();
this.lineLayer.draw();
}
// When dragging we need to update the position of the point
this.updatePt = function(id, pos){
for (var i = 0; i < this.pts.length; i = i + 1){
if (this.pts[i].id === id){
this.pts[i].x = pos.x;
this.pts[i].y = pos.y;
break;
}
}
}
// Function to add and return a line object. We will extend this line to give the appearance of drawing.
this.newLine = function(){
var line = new Konva.Line({
stroke: this.color,
strokeWidth: this.width,
lineCap: 'round',
lineJoin: 'round',
tension : .1
});
this.lineLayer.add(line)
return line;
}
// make pts either side of the split
this.splitPts = function(id, force){
var idx = -1;
// find the pt in the array
for (var i = 0; i < this.pts.length; i = i + 1){
if (this.pts[i].id === id){
idx = i;
if (this.pts[i].fixed && !force){
return null; // we only split once.
}
//break;
}
}
// If idx is -1 we did not find the pt id !
if ( idx === -1){
return null
}
else if (idx === 0 ) {
return null
}
else { // pt not = 0 or max
// We are now going to insert a new pt either side of the one we just dragged
var d1 = this.pts[idx - 1]; // previous pt to the dragged pt
var d2 = this.pts[idx ]; // the pt pt
var d3 = this.pts[idx + 1]; // the next pt after the dragged pt
d2.makeFixed()// flag this pt as no longer splittable
// get point midway from prev pt and dragged pt
var pos = this.getHalfwayPt(d1, d2);
var pt = new DragPoint(this, {x: pos.x, y: pos.y, foxed: false}, false, "C");
pt.draw();
this.pts.splice(idx, 0, pt);
if (d3){
// get point midway from dragged pt to next
pos = this.getHalfwayPt(d2, d3);
var pt = new DragPoint(this, {x: pos.x, y: pos.y, foxed: false}, false, "D");
pt.draw();
this.pts.splice(idx + 2, 0, pt); // note idx + 2 !
}
}
}
// convert last point array entry to handy x,y object.
this.getPoint = function(pts){
return {x: pts[pts.length - 2], y: pts[pts.length - 1]};
}
this.getHalfwayPt = function(d1, d2){
var pos = {
x: d1.x + (d2.x - d1.x)/2,
y: d1.y + (d2.y - d1.y)/2
}
return pos;
}
this.exportPoints = function(){
var list = [], pt;
console.log('pts=' + this.pts.length)
for (var i = 0; i < this.pts.length; i = i + 1){
pt = this.pts[i]
if (pt.fixed){
console.log('push ' + i)
list.push({x: pt.x, y: pt.y});
}
}
return list;
}
}
var route = new Route();
route.lineLayer = lineLayer;
route.ptLayer = pointLayer;
route.fillColor = 'AliceBlue';
route.strokeColor = 'Red';
route.pointOpacity = 0.5;
route.pointRadius = 7;
route.color = '#2982E8'
// Listen for mouse up on the stage to know when to draw points
s1.on('mouseup touchend', function () {
route.addPt(s1.getPointerPosition(), true);
});
// jquery is used here simply as a quick means to make the buttons work.
// Controls for points export
$('#export').on('click', function(){
if ($(this).html() === "Hide"){
$(this).html('Export');
$('#points').hide();
}
else {
$(this).html('Hide');
$('#points')
.css('display', 'block')
.val(JSON.stringify(route.exportPoints()));
}
})
// reset button
$('#reset').on('click', function(){
route.reset();
})
p
{
padding: 4px;
}
#container1
{
background-image: url('https://i.stack.imgur.com/gADDJ.png');
}
#ctrl
{
position: absolute;
z-index: 10;
margin: 0px;
border: 1px solid red;
}
#points
{
width: 500px;
height: 100px;
display: none;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdn.rawgit.com/konvajs/konva/1.6.5/konva.min.js"></script>
<p>Click to add a point, click to add another, drag a point to make a bend, etc.
</p>
<div id='ctrl'>
<button id='reset'>Reset</button>
<button id='export'>Export</button>
<textarea id='points'></textarea>
</div>
<div id='container1' style="display: inline-block; width: 300px, height: 200px; background-color: silver; overflow: hidden; position: relative;"></div>
<div id='img'></div>

Can I create point cloud from depth and rgb image?

I am new to 3js, I have 2 images ie RGB image and Depth image. Can I create a point cloud combining these two using 3js?
if yes then how?
To solve this problem I went the three.js examples and searched for "point". I checked each matching sample for one that had different colors for each particle. Then I clicked the "view source" button to checkout the code. I ended up starting with this example and looked at the source. It made it pretty clear how to make a set of points of different colors.
So after that I just needed to load the 2 images, RGB and Depth, make a grid of points, for each point set the Z position to the depth and the color to the color of the image.
I used my phone to take these RGB and Depth images using this app
To get the data I draw the image into a canvas and then call getImageData. That gives me the data in values from 0 to 255 for each channel, red, green, blue, alpha.
I then wrote a function to get a single pixel out and return the colors in the 0 to 1 range. Just to be safe it checks the boundaries.
// return the pixel at UV coordinates (0 to 1) in 0 to 1 values
function getPixel(imageData, u, v) {
const x = u * (imageData.width - 1) | 0;
const y = v * (imageData.height - 1) | 0;
if (x < 0 || x >= imageData.width || y < 0 || y >= imageData.height) {
return [0, 0, 0, 0];
} else {
const offset = (y * imageData.width + x) * 4;
return Array.from(imageData.data.slice(offset, offset + 4)).map(v => v / 255);
}
}
result
'use strict';
/* global THREE */
function loadImage(url) {
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = "anonymous";
img.onload = (e) => { resolve(img); };
img.onerror = reject;
img.src = url;
});
}
function getImageData(img) {
const ctx = document.createElement("canvas").getContext("2d");
ctx.canvas.width = img.width;
ctx.canvas.height = img.height;
ctx.drawImage(img, 0, 0);
return ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
}
// return the pixel at UV coordinates (0 to 1) in 0 to 1 values
function getPixel(imageData, u, v) {
const x = u * (imageData.width - 1) | 0;
const y = v * (imageData.height - 1) | 0;
if (x < 0 || x >= imageData.width || y < 0 || y >= imageData.height) {
return [0, 0, 0, 0];
} else {
const offset = (y * imageData.width + x) * 4;
return Array.from(imageData.data.slice(offset, offset + 4)).map(v => v / 255);
}
}
async function main() {
const images = await Promise.all([
loadImage("https://i.imgur.com/UKBsvV0.jpg"), // RGB
loadImage("https://i.imgur.com/arPMCZl.jpg"), // Depth
]);
const data = images.map(getImageData);
const canvas = document.querySelector('canvas');
const renderer = new THREE.WebGLRenderer({canvas: canvas});
const fov = 75;
const aspect = 2; // the canvas default
const near = 1;
const far = 4000;
const camera = new THREE.PerspectiveCamera(fov, aspect, near, far);
camera.position.z = 2000;
const controls = new THREE.OrbitControls(camera, canvas);
controls.target.set(0, 0, 0);
controls.update();
const scene = new THREE.Scene();
const rgbData = data[0];
const depthData = data[1];
const skip = 20;
const across = Math.ceil(rgbData.width / skip);
const down = Math.ceil(rgbData.height / skip);
const positions = [];
const colors = [];
const color = new THREE.Color();
const spread = 1000;
const depthSpread = 1000;
const imageAspect = rgbData.width / rgbData.height;
for (let y = 0; y < down; ++y) {
const v = y / (down - 1);
for (let x = 0; x < across; ++x) {
const u = x / (across - 1);
const rgb = getPixel(rgbData, u, v);
const depth = 1 - getPixel(depthData, u, v)[0];
positions.push(
(u * 2 - 1) * spread * imageAspect,
(v * -2 + 1) * spread,
depth * depthSpread,
);
colors.push( ...rgb.slice(0,3) );
}
}
const geometry = new THREE.BufferGeometry();
geometry.addAttribute( 'position', new THREE.Float32BufferAttribute( positions, 3 ) );
geometry.addAttribute( 'color', new THREE.Float32BufferAttribute( colors, 3 ) );
geometry.computeBoundingSphere();
const material = new THREE.PointsMaterial( { size: 15, vertexColors: THREE.VertexColors } );
const points = new THREE.Points( geometry, material );
scene.add( points );
function resizeRendererToDisplaySize(renderer) {
const canvas = renderer.domElement;
const width = canvas.clientWidth;
const height = canvas.clientHeight;
const needResize = canvas.width !== width || canvas.height !== height;
if (needResize) {
renderer.setSize(width, height, false);
}
return needResize;
}
function render(time) {
time *= 0.001;
if (resizeRendererToDisplaySize(renderer)) {
const canvas = renderer.domElement;
camera.aspect = canvas.clientWidth / canvas.clientHeight;
camera.updateProjectionMatrix();
}
renderer.render(scene, camera);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
}
main();
body {
margin: 0;
}
canvas {
width: 100vw;
height: 100vh;
display: block;
}
<canvas></canvas>
<script src="https://threejsfundamentals.org/threejs/resources/threejs/r94/three.min.js"></script>
<script src="https://threejsfundamentals.org/threejs/resources/threejs/r94/js/controls/OrbitControls.js"></script>

Keep changed position of NVD3 legend after click

I am trying to change nvd3 chart legend position to the bottom of the chart.
I've heard about the:
d3.select(.nv-legendWrap).attr("transform", "translate(x, y)")
It's actually working but a click action on the legend will replace it at the top of the chart.
And I don't get how to use de legendPostion function (that is not a function according to the console).
After doing this (mind the quotes):
d3.select(".nv-legendWrap").attr("transform", "translate(x,y)")
Which will, as you know, move the legend to another position, set the same position when you click it:
d3.select(".nv-legendWrap").on("click", function(){
d3.select(this).attr("transform", "translate(x,y)");
})
Here is an example, the original position of the legend is on the top, but I'm moving it to the bottom. Click it and it will remain in the same position:
var data = function() {
return stream_layers(3,10+Math.random()*100,.1).map(function(data, i) {
return {
key: 'Stream' + i,
values: data
};
});
}
/* Inspired by Lee Byron's test data generator. */
function stream_layers(n, m, o) {
if (arguments.length < 3) o = 0;
function bump(a) {
var x = 1 / (.1 + Math.random()),
y = 2 * Math.random() - .5,
z = 10 / (.1 + Math.random());
for (var i = 0; i < m; i++) {
var w = (i / m - y) * z;
a[i] += x * Math.exp(-w * w);
}
}
return d3.range(n).map(function() {
var a = [], i;
for (i = 0; i < m; i++) a[i] = o + o * Math.random();
for (i = 0; i < 5; i++) bump(a);
return a.map(stream_index);
});
}
/* Another layer generator using gamma distributions. */
function stream_waves(n, m) {
return d3.range(n).map(function(i) {
return d3.range(m).map(function(j) {
var x = 20 * j / m - i / 3;
return 2 * x * Math.exp(-.5 * x);
}).map(stream_index);
});
}
function stream_index(d, i) {
return {x: i, y: Math.max(0, d)};
}
nv.addGraph(function() {
var chart = nv.models.multiBarChart();
chart.xAxis
.tickFormat(d3.format(',f'));
chart.yAxis
.tickFormat(d3.format(',.1f'));
chart.multibar.stacked(true); // default to stacked
chart.showControls(false); // don't show controls
d3.select('#chart svg')
.datum(data())
.transition().duration(500).call(chart);
nv.utils.windowResize(chart.update);
d3.select(".nv-legendWrap").attr("transform", "translate(0,340)");
d3.select(".nv-legendWrap").on("click", function(){
d3.select(this).attr("transform", "translate(0,340)");
})
return chart;
});
#chart svg {
height: 400px;
width: 600px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/nvd3/1.8.5/nv.d3.min.js"></script>
<div id="chart">
<svg></svg>
</div>
Have in mind that the legendPosition method is quite limited, you can only choose top or right:
chart.legendPosition("top")
chart.legendPosition("right")

How do i bind onclick event to piechart segment?

How do i bind onclick event to piechart segment?
https://github.com/sauminkirve/HTML5/blob/master/PieChart/piechart.html
A pie chart segment is really a wedge. You have several ways to hit-test a wedge.
One way is the math way:
Test if the mouse is within the radius of a circle created by the wedges.
If the radius test is true, then calculate the angle of the mouse versus the circle's centerpoint.
Compare that angle to each wedge. If the angle is between the starting and ending angle of a specific wedge's arc, then the mouse is inside that wedge.
Another way is to use canvas's built in path hit-testing method: isPointInPath
Redefine one wedge. There's no need to actually stroke or fill that wedge. Just do the commands from beginPath to closePath.
Use context.isPointInPath(mouseX,mouseY) to hit-test if the mouse is inside that wedge.
If isPointInPath returns true, you've discovered the wedge under the mouse. If not, then redefine & hit-test each of the other wedges.
Here's something I coded a while back that hit-tests the wedges of a pie chart when hovering and moves the wedge out of the pie when a wedge is clicked.
It uses the isPointInPath method to do the hit-testing:
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
ctx.lineJoin = "round";
var $canvas = $("#canvas");
var canvasOffset = $canvas.offset();
var offsetX = canvasOffset.left;
var offsetY = canvasOffset.top;
var scrollX = $canvas.scrollLeft();
var scrollY = $canvas.scrollTop();
function Wedge(cx, cy, radius, startAngleDeg, endAngleDeg, fill, stroke, linewidth) {
this.cx = cx;
this.cy = cy;
this.radius = radius;
this.startAngle = startAngleDeg * Math.PI / 180;
this.endAngle = endAngleDeg * Math.PI / 180;
this.fill = fill;
this.stroke = stroke;
this.lineWidth = linewidth;
this.offsetX = 0;
this.offsetY = 0;
this.rr = radius * radius;
this.centerX = cx;
this.centerY = cy;
this.midAngle = this.startAngle + (this.endAngle - this.startAngle) / 2;
this.offsetDistance = 15;
this.explodeX = this.offsetDistance * Math.cos(this.midAngle);
this.explodeY = this.offsetDistance * Math.sin(this.midAngle);
this.isExploded = false;
};
Wedge.prototype.draw = function(fill, stroke) {
this.define();
this.fillStroke(fill, stroke);
ctx.beginPath();
ctx.arc(this.cx, this.cy, this.radius, 0, Math.PI * 2);
ctx.closePath();
ctx.lineWidth = 0.50;
ctx.stroke();
}
Wedge.prototype.fillStroke = function(fill, stroke) {
ctx.fillStyle = fill || this.fill;
ctx.fill();
ctx.strokeStyle = stroke, this.stroke;
ctx.lineWidth = this.lineWidth;
ctx.stroke();
}
Wedge.prototype.define = function() {
var x = this.cx + this.offsetX;
var y = this.cy + this.offsetY;
ctx.beginPath();
ctx.arc(x, y, this.radius, this.startAngle, this.endAngle);
ctx.lineTo(x, y);
ctx.closePath();
}
Wedge.prototype.ptAtAngle = function(radianAngle) {
var xx = (this.cx + this.offsetX) + this.radius * Math.cos(radianAngle);
var yy = (this.cy + this.offsetY) + this.radius * Math.sin(radianAngle);
return ({
x: x,
y: y
});
}
Wedge.prototype.explode = function(isExploded) {
this.isExploded = isExploded;
this.offsetX = isExploded ? this.explodeX : 0;
this.offsetY = isExploded ? this.explodeY : 0;
this.draw();
}
Wedge.prototype.isPointInside = function(x, y) {
var dx = x - (this.cx + this.offsetX);
var dy = y - (this.cy + this.offsetY);
if (dx * dx + dy * dy > this.rr) {
return (false);
}
var angle = (Math.atan2(dy, dx) + Math.PI * 2) % (Math.PI * 2);
return (angle >= this.startAngle && angle <= this.endAngle);
}
Wedge.prototype.marker = function(pos) {
ctx.beginPath();
ctx.arc(pos.x, pos.y, 3, 0, Math.PI * 2);
ctx.closePath();
ctx.fillStyle = "red";
ctx.fill();
}
function handleMouseDown(e) {
e.preventDefault();
mouseX = parseInt(e.clientX - offsetX);
mouseY = parseInt(e.clientY - offsetY);
clear();
for (var i = 0; i < wedges.length; i++) {
var wedge = wedges[i].wedge;
if (wedge.isPointInside(mouseX, mouseY)) {
wedge.explode(!wedge.isExploded);
}
wedge.draw();
}
}
function handleMouseUp(e) {
e.preventDefault();
mouseX = parseInt(e.clientX - offsetX);
mouseY = parseInt(e.clientY - offsetY);
// Put your mouseup stuff here
isDown = false;
}
function handleMouseOut(e) {
e.preventDefault();
mouseX = parseInt(e.clientX - offsetX);
mouseY = parseInt(e.clientY - offsetY);
// Put your mouseOut stuff here
isDown = false;
}
function handleMouseMove(e) {
e.preventDefault();
mouseX = parseInt(e.clientX - offsetX);
mouseY = parseInt(e.clientY - offsetY);
for (var i = 0; i < wedges.length; i++) {
var wedge = wedges[i].wedge;
if (wedge.isPointInside(mouseX, mouseY)) {
wedge.draw("black");
} else {
wedge.draw();
}
}
}
$("#canvas").mousedown(function(e) {
handleMouseDown(e);
});
$("#canvas").mousemove(function(e) {
handleMouseMove(e);
});
$("#canvas").mouseup(function(e) {
handleMouseUp(e);
});
$("#canvas").mouseout(function(e) {
handleMouseOut(e);
});
function clear() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
var PI2 = Math.PI * 2;
var cx = 150;
var cy = 150;
var r = 100;
var line = 2;
var stroke = "black";
var wedges = [];
wedges.push({
percent: 18,
fill: "red"
});
wedges.push({
percent: 30,
fill: "blue"
});
wedges.push({
percent: 25,
fill: "green"
});
wedges.push({
percent: 13,
fill: "purple"
});
wedges.push({
percent: 14,
fill: "gold"
});
var rAngle = 0;
for (var i = 0; i < wedges.length; i++) {
var wedge = wedges[i];
var angle = 360 * wedge.percent / 100;
wedge.wedge = new Wedge(cx, cy, r, rAngle, rAngle + angle, wedge.fill, "black", 1);
wedge.wedge.draw();
rAngle += angle;
}
window.onscroll = function(e) {
var BB = canvas.getBoundingClientRect();
offsetX = BB.left;
offsetY = BB.top;
}
body {
background-color: ivory;
}
#canvas {
border: 1px solid red;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<h4>Hover wedge to highlight it<br>Click wedge to explode that wedge</h4>
<canvas id="canvas" width=300 height=300></canvas>

Resources