Highcharts manually added svg elements not following stock graph on pan - javascript

I have a click event in my highstock / highchart graph, I have successfully added custom drawing tools such as adding lines and text. Here is the code for that
$('#stockchart-canvas-container').on('click','svg',function(e){
var svg = $('#stockchart-canvas-container svg')[0];
var point= svg.createSVGPoint(), svgP
point.x = e.clientX
point.y = e.clientY
svgP = point.matrixTransform(svg.getScreenCTM().inverse());
if(user.selected_tool=='line'){
if(user.previous_x == undefined && user.previous_y == undefined) {
user.current_x = svgP.x
user.current_y = svgP.y
user.previous_x = 0
user.previous_y = 0
$('#stockchart-canvas-container').on('mousemove','svg',function(ev){
var svg2 = $('#stockchart-canvas-container svg')[0];
var point2= svg.createSVGPoint(), svgP2
point2.x = ev.clientX
point2.y = ev.clientY
svgP2 = point2.matrixTransform(svg2.getScreenCTM().inverse());
$('#temp-line').remove()
stockchart.renderer.path(['M',
user.current_x,
user.current_y,
'L',
svgP2.x,
svgP2.y,
'Z',
]).attr({'stroke-width':2,stroke:'#ccc',id:'temp-line'}).add(stockchart.seriesGroup)
})
} else {
$('#stockchart-canvas-container').off('mousemove')
stockchart.renderer.path(['M',
user.current_x,
user.current_y,
'L',
svgP.x,
svgP.y,
'Z'
]).attr({'stroke-width':2,stroke:'#ccc'}).add(stockchart.seriesGroup)
user.current_x=0
user.current_y=0
user.previous_x=undefined
user.previous_y=undefined
}
} else if (user.selected_tool=='text') {
$('#insert-text-modal').modal('show')
$('#accept-insert-text').on('click',function(){
if($('#text-input').val()){
stockchart.renderer.text($('#text-input').val(),svgP.x,svgP.y).add(stockchart.seriesGroup)
}
$(this).off('click')
$('#insert-text-modal').modal('hide')
})
}
})
My problem is that I want the line and the text to follow the stock graph as I pan or zoom the graph. Any ideas how I can do this?

You have to preserve coordinate values at the moment the text/line is drawn - the coordinates in terms of axes. On each chart redraw, you need to reposition the line/text - so you have to calculate new pixel position (which can be calculated via axis.toPixels) and set the new values to the line/text. For a text you need to calculate one point, for a path element you need to recalculate each segment.
See the code below:
Function for calculating pixels from values and values from pixels - it includes some basic logic for hiding a text if it overflows a chart's plot area - but it should be adjusted depending on your needs.
function translate (x, y, chart, toPixels) {
const xAxis = chart.xAxis[0]
const yAxis = chart.yAxis[0]
let tx, ty, hide
if (toPixels) {
tx = xAxis.toPixels(x)
ty = yAxis.toPixels(y)
if (tx < xAxis.left || tx > xAxis.left + xAxis.width) {
hide = true
} else if (!hide && (ty < yAxis.top || ty > yAxis.top + yAxis.height)) {
hide = true
}
if (hide) {
tx = -9e7
ty = -9e7
}
} else {
tx = xAxis.toValue(x)
ty = yAxis.toValue(y)
}
return { x: tx, y: ty }
}
On chart click - it adds the text and keep in the array, on chart redraw r - it repositions items.
chart: {
events: {
load: function () {
this.drawnItems = []
},
click: function (e) {
const { x, y } = e
const text = this.renderer.text('custom text', x, y).add()
text.point = translate(x, y, this)
this.drawnItems.push(text)
},
redraw: function () {
this.drawnItems.forEach(item => {
const { x, y } = item.point
item.attr(translate(x, y, this, true))
})
}
}
},
Live example: http://jsfiddle.net/nsf67ro6/

Related

Implement zoom in datamaps

I have a globe with datamaps, and I would like to implement a little zoom feature into it. Not zooming to specific countries, just a normal zoom feature.
Where should I look for it? I'm new to d3 as well as datamaps. I managed to customize it, more or less, but this zoom function is still missing.
Here's the codepen:
https://codepen.io/simii/pen/yZvQmv
And my javascript:
//basic map config with custom fills, mercator projection
var series = [
["USA",36.2],["GBR",7.4],["CAN",6.2],["DEU",5.7],["FRA", 4.1],["ESP",4.1],["ITA",3.3],["MEX",3.0],["AUS",2.5],["NLD",2.4],
["IND",2.1],["BRA",2.0],["GRC",1.4],["AUT",1.2],["ROU",1.2],["SRB",1.0],["COL",0.8],["POL",0.8],["ZAF",0.7],["SWE",0.7],
["DNK",0.6],["VEN",0.6],["JPN",0.6],["KOR",0.6],["BEL",0.5],["RUS",0.5],["PRT",0.5]
];
var dataset = {};
// We need to colorize every country based on "percent"
// colors should be uniq for every value.
// For this purpose we create palette(using min/max series-value)
var onlyValues = series.map(function(obj){ return obj[1]; });
var minValue = Math.min.apply(null, onlyValues),
maxValue = Math.max.apply(null, onlyValues);
// create color palette function
// color can be whatever you wish
var paletteScale = d3.scale.linear()
.domain([minValue,maxValue])
.range(["rgb(0,0,0)","rgb(219,219,219)"]); // color
// fill dataset in appropriate format
series.forEach(function(item){ //
// item example value ["USA", 36.2]
var iso = item[0],
value = item[1];
dataset[iso] = { percent: value, fillColor: paletteScale(value) };
});
var map;
var globalRotation = [90,-30];
function redraw() {
d3.select("#world").html('');
init();
}// redraw
function init() {
map = new Datamap({//need global var
scope: 'world',
element: document.getElementById('world'),
projection: 'orthographic',
projectionConfig: {
rotation: globalRotation
},
fills: {defaultFill: 'rgba(30,30,30,0.1)'},
data: dataset,
geographyConfig: {
responsive: true,
borderColor: 'rgba(222,222,222,0.2)',
highlightBorderWidth: 1,
// don't change color on mouse hover
highlightFillColor: function(geo) {
return geo['fillColor'] || 'rgba(30,30,30,0.5)';
},
// only change border
highlightBorderColor: 'rgba(222,222,222,0.5)',
// show desired information in tooltip
popupTemplate: function(geo, data) {
// don't show tooltip if country don't present in dataset
if (!data) { return ; }
// tooltip content
return ['',
'<div style="opacity:0.7;" class="hoverinfo">% of visitors in ' + geo.properties.name,
': ' + data.percent,
''].join('');
}
}
});
//draw a legend for this map
map.legend();
map.graticule();
var drag = d3.behavior.drag().on('drag', function() {
var dx = d3.event.dx;
var dy = d3.event.dy;
// var rotation = livemapScope.rotation;
var rotation = map.projection.rotate();
var radius = map.projection.scale();
var scale = d3.scale.linear()
.domain([-1 * radius, radius])
.range([-90, 90]);
var degX = scale(dx);
var degY = scale(dy);
rotation[0] += degX;
rotation[1] -= degY;
if (rotation[1] > 90) rotation[1] = 90;
if (rotation[1] < -90) rotation[1] = -90;
if (rotation[0] >= 180) rotation[0] -= 360;
globalRotation = rotation;
redraw();
})
d3.select("#world").select("svg").call(drag);
}// init
redraw();

zoom by mouse wheel on canvas Angular+D3

I want to make zoom on HTML canvas by mouse wheel like on google map.
I have two canvases on my project, one for showing an image and one for edit the image. I do for both of the canvases transformation and scale according to the mouse position.
Everything goes fine. the zoom and the draw after the zoom. the problem is when I try to save the original position of the drawing after the zoom - the position is not correct.
HTML:
<canvas id="backgroundCanvas" #backgroundCanvas class="background-canvas" width="930" height="710"></canvas>
<canvas #editCanvas mouseWheel class="edit-canvas" width="930" height="710" [style.cursor]="editMode ? 'pointer' : '-webkit-grab'" (mouseWheelUp)="mouseWheelUpFunc($event)" (mouseWheelDown)="mouseWheel($event)"></canvas>
JS:
this.canvas1 = d3.select("#editCanvas");
this.context = this.canvas1.node().getContext("2d");
this.width = this.canvas1.property("width");
this.height = this.canvas1.property("height");
this.canvas1.call(d3.zoom()
.scaleExtent([1, 4])
.on("zoom", () => {
if (this.zoomStep === 0) {
this.isZoomed = false; //set isZoomed to false for disable to drag the image
}
var wheel = d3.event.sourceEvent.wheelDelta/120;//n or -n
var zoom = 0;
if(wheel < 0)//if it's zoom out
{
this.zoomStep--;
this.setCurrentScale(this.zoomFactor, false);
this.resetBackgroundTopLeft();
console.log("wheel is under 0 ");
zoom = 1/1.05;
}
else//if it's zoom in
{
this.zoomStep++;
this.isZoomed = true;//set isZoomed to true for enable to drag the image
this.setCurrentScale(this.zoomFactor, true);
zoom = 1.05;
}
this.currentzoom *= zoom; //set the current zoom for know whem to stop
this.clearBackgroundCanvas(); // clear the background image befor draw it again.
/*********************************change the background canvas (where the image is)****************************************************/
this.backgroundContext.save();
this.backgroundContext.translate(
d3.event.transform.x,
d3.event.transform.y
);
this.backgroundContext.scale(d3.event.transform.k, d3.event.transform.k);
this.backgroundContext.drawImage(this.curentImage, this.imageTopLeft.x, this.imageTopLeft.y ,this.img.w * this.currentScale, this.img.h * this.currentScale);
this.backgroundContext.restore();
/***********************************change the edit canvas (where the tags are)**************************************************/
this.editContext.save();
this.editContext.translate(
d3.event.transform.x,
d3.event.transform.y
);
this.editContext.scale(d3.event.transform.k, d3.event.transform.k);
this.draw() //clear and then draw the marker again
this.editContext.restore();
/*************************************************************************************/
}))
.on("mousedown.zoom", () =>{
this.onMouseDown1(d3.event);
})
/**This function set the sacle to zoom In or Out according the mouse wheel*/
setCurrentScale = (scale: number, zoomIn: boolean) => {
this.currentScale *= zoomIn ? scale : 1 / scale;
}
/**This function clear all the canvase area */
clearBackgroundCanvas() {
this.backgroundContext.clearRect(0, 0, this.canvas.w, this.canvas.h);
}
onMouseDown1(evt: MouseEvent) {
var coordinates = d3.mouse(this.canvas1.node());
console.log("onMouseDown" + evt.offsetX , evt.offsetY ,coordinates)
if (this.editMode) {
if (this.isInDrawingArea(evt.offsetX, evt.offsetY, this.imageTopLeft)) {
// return relative mouse position
this.currentDrawnMarkerStart = {
x: coordinates[0],
y: coordinates[1]
};
this.isDrawingRect = true;
if(this.scale == null)
this.scale ={k :1 };
this.drawRectBorder(
this.currentDrawnMarkerStart.x ,
this.currentDrawnMarkerStart.y ,
250*this.currentScale*this.scale.k,
250*this.currentScale*this.scale.k,
'#004de6',2*this.scale.k);//color for the rect tag on drawing
const m = {
x: this.currentDrawnMarkerStart.x,
y: this.currentDrawnMarkerStart.y,
w: 250*this.currentScale,
h: 250*this.currentScale,
isSelected: false,
isHovered: false
};
this.addMarker(m);
}
} else if (!this.isDragging && this.isZoomed) {
this.dragStart = {
x: evt.offsetX,
y: evt.offsetY
};
this.isDragging = true;
}
}
drawRectBorder = (x0: number, y0: number, w: number, h: number, color: string, borderW: number = 1) => {
this.editContext.strokeStyle = color;
this.editContext.lineWidth = borderW;
this.editContext.beginPath();
this.editContext.rect(x0, y0, w, h);
this.editContext.stroke();
this.editContext.closePath();
}
addMarker = (r: IMarker) => {
console.log(this.currentScale)
const original = editorUtil.transformMarkerToOriginal(editorUtil.getMarkerTopLeft(r),
this.imageTopLeft, this.currentScale);
// Save coordinates on canvas
this.currentMarkers.push(original);
console.log(original)
}
/** Get the marker's top left corner, with absolute values for w,h */
export const getMarkerTopLeft = (m: IMarker): IMarker => {
const res: IMarker = {
x: m.x + (m.w < 0 ? m.w : 0),
y: m.y + (m.h < 0 ? m.h : 0),
w: Math.abs(m.w),
h: Math.abs(m.h),
isSelected: m.isSelected,
isHovered: m.isHovered
};
return res;
};
/** Get the marker's coordinates with regards to the original image dimensions */
export const transformMarkerToOriginal = (m: IMarker, imageTopLeft: Coordinate, scale: number): IMarker => {
const res: IMarker = {
x: Math.floor((m.x - imageTopLeft.x) / scale),
y: Math.floor((m.y - imageTopLeft.y) / scale),
w: Math.floor(Math.abs(m.w / scale)),
h: Math.floor(Math.abs(m.h / scale)),
isSelected: false,
isHovered: false
};
return res;
};
Thank's!!

D3 semantic zooming with Reusable Pattern

I'm trying to implement semantic zooming while using Mike Bostock's Towards Reusable Charts pattern (where a chart is represented as a function). In my zoom handler, I'd like to use transform.rescaleX to update my scale and then simply call the function again.
It almost works but the rescaling seems to accumulate zoom transforms getting faster and faster. Here's my fiddle:
function chart() {
let aspectRatio = 10.33;
let margin = { top: 0, right: 0, bottom: 5, left: 0 };
let current = new Date();
let scaleBand = d3.scaleBand().padding(.2);
let scaleTime = d3.scaleTime().domain([d3.timeDay(current), d3.timeDay.ceil(current)]);
let axis = d3.axisBottom(scaleTime);
let daysThisMonth = d3.timeDay.count(d3.timeMonth(current), d3.timeMonth.ceil(current));
let clipTypes = [ClipType.Scheduled, ClipType.Alarm, ClipType.Motion];
let zoom = d3.zoom().scaleExtent([1 / daysThisMonth, 1440]);
let result = function(selection) {
selection.each(function(data) {
let selection = d3.select(this);
let outerWidth = this.getBoundingClientRect().width;
let outerHeight = outerWidth / aspectRatio;
let width = outerWidth - margin.left - margin.right;
let height = outerHeight - margin.top - margin.bottom;
scaleBand.domain(d3.range(data.length)).range([0, height * .8]);
scaleTime.range([0, width]);
zoom.on('zoom', _ => {
scaleTime = d3.event.transform.rescaleX(scaleTime);
selection.call(result);
});
let svg = selection.selectAll('svg').data([data]);
let svgEnter = svg.enter().append('svg').attr('viewBox', '0 0 ' + outerWidth + ' ' + outerHeight);//.attr('preserveAspectRatio', 'xMidYMin slice');
svg = svg.merge(svgEnter);
let defsEnter = svgEnter.append('defs');
let defs = svg.select('defs');
let gMainEnter = svgEnter.append('g').attr('id', 'main');
let gMain = svg.select('g#main').attr('transform', 'translate(' + margin.left + ' ' + margin.top + ')');
let gAxisEnter = gMainEnter.append('g').attr('id', 'axis');
let gAxis = gMain.select('g#axis').call(axis.scale(scaleTime));
let gCameraContainerEnter = gMainEnter.append('g').attr('id', 'camera-container');
let gCameraContainer = gMain.select('g#camera-container').attr('transform', 'translate(' + 0 + ' ' + height * .2 + ')').call(zoom);
let gCameraRowsEnter = gCameraContainerEnter.append('g').attr('id', 'camera-rows');
let gCameraRows = gCameraContainer.select('g#camera-rows');
let gCameras = gCameraRows.selectAll('g.camera').data(d => {
return d;
});
let gCamerasEnter = gCameras.enter().append('g').attr('class', 'camera');
gCameras = gCameras.merge(gCamerasEnter);
gCameras.exit().remove();
let rectClips = gCameras.selectAll('rect.clip').data(d => {
return d.clips.filter(clip => {
return clipTypes.indexOf(clip.type) !== -1;
});
});
let rectClipsEnter = rectClips.enter().append('rect').attr('class', 'clip').attr('height', _ => {
return scaleBand.bandwidth();
}).attr('y', (d, i, g) => {
return scaleBand(Array.prototype.indexOf.call(g[i].parentNode.parentNode.childNodes, g[i].parentNode)); //TODO: sloppy
}).style('fill', d => {
switch(d.type) {
case ClipType.Scheduled:
return '#0F0';
case ClipType.Alarm:
return '#FF0';
case ClipType.Motion:
return '#F00';
};
});
rectClips = rectClips.merge(rectClipsEnter).attr('width', d => {
return scaleTime(d.endTime) - scaleTime(d.startTime);
}).attr('x', d => {
return scaleTime(d.startTime);
});
rectClips.exit().remove();
let rectBehaviorEnter = gCameraContainerEnter.append('rect').attr('id', 'behavior').style('fill', '#000').style('opacity', 0);
let rectBehavior = gCameraContainer.select('rect#behavior').attr('width', width).attr('height', height * .8);//.call(zoom);
});
};
return result;
}
// data model
let ClipType = {
Scheduled: 0,
Alarm: 1,
Motion: 2
};
let data = [{
id: 1,
src: "assets/1.jpg",
name: "Camera 1",
server: 1
}, {
id: 2,
src: "assets/2.jpg",
name: "Camera 2",
server: 1
}, {
id: 3,
src: "assets/1.jpg",
name: "Camera 3",
server: 2
}, {
id: 4,
src: "assets/1.jpg",
name: "Camera 4",
server: 2
}].map((_ => {
let current = new Date();
let randomClips = d3.randomUniform(24);
let randomTimeSkew = d3.randomUniform(-30, 30);
let randomType = d3.randomUniform(3);
return camera => {
camera.clips = d3.timeHour.every(Math.ceil(24 / randomClips())).range(d3.timeDay.offset(current, -30), d3.timeDay(d3.timeDay.offset(current, 1))).map((d, indexEndTime, g) => {
return {
startTime: indexEndTime === 0 ? d : d3.timeMinute.offset(d, randomTimeSkew()),
endTime: indexEndTime === g.length - 1 ? d3.timeDay(d3.timeDay.offset(current, 1)) : null,
type: Math.floor(randomType())
};
}).map((d, indexStartTime, g) => {
if(d.endTime === null)
d.endTime = g[indexStartTime + 1].startTime;
return d;
});
return camera;
};
})());
let myChart = chart();
let selection = d3.select('div#container');
selection.datum(data).call(myChart);
<div id="container"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.13.0/d3.min.js"></script>
Edit: The zoom handler below works fine, but I'd like a more general solution:
let newScaleTime = d3.event.transform.rescaleX(scaleTime);
d3.select('g#axis').call(axis.scale(newScaleTime));
d3.selectAll('rect.clip').attr('width', d => {
return newScaleTime(d.endTime) - newScaleTime(d.startTime);
}).attr('x', d => {
return newScaleTime(d.startTime);
});
The short answer is you need to implement a reference scale to indicate what the scale's base state is when unmanipulated by the zoom. Otherwise you will run into the problem you describe: "It almost works but the rescaling seems to accumulate zoom transforms getting faster and faster. "
To see why a reference scale is needed, zoom in on the graph and out (once each) without moving the mouse. When you zoom in, the axis changes. When you zoom out the axis does not. Note the scale factor on the intial zoom in and the first time you zoom out: 1.6471820345351462 on the zoom in, 1 on the zoom out. The number represents how much the to magnify/minify whatever it is we are zooming in on. On the initial zoom in we magnify by a factor of ~1.65. On the preceding zoom out we minify by a factor of 1, ie: not at all. If on the other hand you zoom out first, you minify by a factor of about 0.6 and then if you were to zoom in you magnify by a factor of 1. I've built a stripped down of your example to show this:
function chart() {
let zoom = d3.zoom().scaleExtent([0.25,20]);
let scale = d3.scaleLinear().domain([0,1000]).range([0,550]);
let axis = d3.axisBottom;
let result = function(selection) {
selection.each(function() {
let selection = d3.select(this);
selection.call(axis(scale));
selection.call(zoom);
zoom.on('zoom', function() {
scale = d3.event.transform.rescaleX(scale);
console.log(d3.event.transform.k);
selection.call(result);
});
})
}
return result;
}
d3.select("svg").call(chart());
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
<svg width="550" height="200"></svg>
The scale should be relative to the initial zoom factor, usually 1. In otherwords, the zoom is cumulative, it records magnification/minification as a factor of the initial scale, not the last step (otherwise transform k values would only be one of three values: one value for zooming out, another for zooming in and one for remaining the same and all relative to the current scale). This is why rescaling the initial scale doesn't work - you lose the reference point to the initial scale that the zoom is referencing.
From the docs, if you redefine a scale with d3.event.transform.rescaleX, we get a scale that reflects the zoom's (cumulative) transformation:
[the rescaleX] method does not modify the input scale x; x thus
represents the untransformed scale, while the returned scale
represents its transformed view. (docs)
Building on this, if we zoom in twice in a row, the first time we zoom in we see the transform.k value is ~1.6x on the first time, the second time it is ~2.7x. But, since we rescale the scale, we apply a zoom of 2.7x on a scale that has already been zoomed in 1.6x, giving us a scale factor of ~4.5x rather than 2.7x. To make matters worse, if we zoom in twice and then out once, the zoom (out) event gives us a scale value that is still greater than 1 (~1.6 on first zoom in, ~2.7 on second, ~1.6 on zoom out), hence we are still zooming in despite scrolling out:
function chart() {
let zoom = d3.zoom().scaleExtent([0.25,20]);
let scale = d3.scaleLinear().domain([0,1000]).range([0,550]);
let axis = d3.axisBottom;
let result = function(selection) {
selection.each(function() {
let selection = d3.select(this);
selection.call(axis(scale));
selection.call(zoom);
zoom.on('zoom', function() {
scale = d3.event.transform.rescaleX(scale);
var magnification = 1000/(scale.domain()[1] - scale.domain()[0]);
console.log("Actual magnification: "+magnification+"x");
console.log("Intended magnification: "+d3.event.transform.k+"x")
console.log("---");
selection.call(result);
});
})
}
return result;
}
d3.select("svg").call(chart());
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
<svg width="550" height="200"></svg>
I haven't discussed the x offset portion of the zoom, but you can imagine that a similar problem occurs - the zoom is cumulative but you lose the initial reference point that those cumulative changes are in reference to.
The idiomatic solution is to use a reference scale and the zoom to create a working scale used for plotting rectangles/axes/etc. The working scale is initially the same as the reference scale (generally) and is set as so: workingScale = d3.event.transform.rescaleX(referenceScale) on each zoom.
function chart() {
let zoom = d3.zoom().scaleExtent([0.25,20]);
let workingScale = d3.scaleLinear().domain([0,1000]).range([0,550]);
let referenceScale = d3.scaleLinear().domain([0,1000]).range([0,550]);
let axis = d3.axisBottom;
let result = function(selection) {
selection.each(function() {
let selection = d3.select(this);
selection.call(axis(workingScale));
selection.call(zoom);
zoom.on('zoom', function() {
workingScale = d3.event.transform.rescaleX(referenceScale);
var magnification = 1000/(workingScale.domain()[1] - workingScale.domain()[0]);
console.log("Actual magnification: "+magnification+"x");
console.log("Intended magnification: "+d3.event.transform.k+"x")
console.log("---");
selection.call(result);
});
})
}
return result;
}
d3.select("svg").call(chart());
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
<svg width="550" height="200"></svg>

pie chart use line point to percentage text [duplicate]

I am using Chart.js for drawing pie chart in my php page.I found tooltip as showing each slice values.
But I wish to display those values like below image.
I do not know how to do this with chart.js.
Please help me.
My Javascript code:
function drawPie(canvasId,data,legend){
var ctx = $("#pie-canvas-" + canvasId).get(0).getContext("2d");
var piedata = [];
$.each(data,function(i,val){
piedata.push({value:val.count,color:val.color,label:val.status});
});
var options =
{
tooltipTemplate: "<%= Math.round(circumference / 6.283 * 100) %>%",
}
var pie = new Chart(ctx).Pie(piedata,options);
if(legend)document.getElementById("legend").innerHTML = pie.generateLegend();
}
php code:
printf('<table><tr>');
echo '<td style="text-align: right;"><canvas id="pie-canvas-'
. $canvasId
. '" width="256" height="256" ></canvas></td><td style="text-align: left;width:360px;height:auto" id="legend" class="chart-legend"></td></tr></table>';
echo '<script type="text/javascript">drawPie('
. $canvasId
. ', '
. $data3
.', '
. $legend
. ');</script>';
For Chart.js 2.0 and up, the Chart object data has changed. For those who are using Chart.js 2.0+, below is an example of using HTML5 Canvas fillText() method to display data value inside of the pie slice. The code works for doughnut chart, too, with the only difference being type: 'pie' versus type: 'doughnut' when creating the chart.
Script:
Javascript
var data = {
datasets: [{
data: [
11,
16,
7,
3,
14
],
backgroundColor: [
"#FF6384",
"#4BC0C0",
"#FFCE56",
"#E7E9ED",
"#36A2EB"
],
label: 'My dataset' // for legend
}],
labels: [
"Red",
"Green",
"Yellow",
"Grey",
"Blue"
]
};
var pieOptions = {
events: false,
animation: {
duration: 500,
easing: "easeOutQuart",
onComplete: function () {
var ctx = this.chart.ctx;
ctx.font = Chart.helpers.fontString(Chart.defaults.global.defaultFontFamily, 'normal', Chart.defaults.global.defaultFontFamily);
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
this.data.datasets.forEach(function (dataset) {
for (var i = 0; i < dataset.data.length; i++) {
var model = dataset._meta[Object.keys(dataset._meta)[0]].data[i]._model,
total = dataset._meta[Object.keys(dataset._meta)[0]].total,
mid_radius = model.innerRadius + (model.outerRadius - model.innerRadius)/2,
start_angle = model.startAngle,
end_angle = model.endAngle,
mid_angle = start_angle + (end_angle - start_angle)/2;
var x = mid_radius * Math.cos(mid_angle);
var y = mid_radius * Math.sin(mid_angle);
ctx.fillStyle = '#fff';
if (i == 3){ // Darker text color for lighter background
ctx.fillStyle = '#444';
}
var percent = String(Math.round(dataset.data[i]/total*100)) + "%";
//Don't Display If Legend is hide or value is 0
if(dataset.data[i] != 0 && dataset._meta[0].data[i].hidden != true) {
ctx.fillText(dataset.data[i], model.x + x, model.y + y);
// Display percent in another line, line break doesn't work for fillText
ctx.fillText(percent, model.x + x, model.y + y + 15);
}
}
});
}
}
};
var pieChartCanvas = $("#pieChart");
var pieChart = new Chart(pieChartCanvas, {
type: 'pie', // or doughnut
data: data,
options: pieOptions
});
HTML
<canvas id="pieChart" width=200 height=200></canvas>
jsFiddle
I found an excellent Chart.js plugin that does exactly what you want:
https://github.com/emn178/Chart.PieceLabel.js
From what I know I don't believe that Chart.JS has any functionality to help for drawing text on a pie chart. But that doesn't mean you can't do it yourself in native JavaScript. I will give you an example on how to do that, below is the code for drawing text for each segment in the pie chart:
function drawSegmentValues()
{
for(var i=0; i<myPieChart.segments.length; i++)
{
// Default properties for text (size is scaled)
ctx.fillStyle="white";
var textSize = canvas.width/10;
ctx.font= textSize+"px Verdana";
// Get needed variables
var value = myPieChart.segments[i].value;
var startAngle = myPieChart.segments[i].startAngle;
var endAngle = myPieChart.segments[i].endAngle;
var middleAngle = startAngle + ((endAngle - startAngle)/2);
// Compute text location
var posX = (radius/2) * Math.cos(middleAngle) + midX;
var posY = (radius/2) * Math.sin(middleAngle) + midY;
// Text offside to middle of text
var w_offset = ctx.measureText(value).width/2;
var h_offset = textSize/4;
ctx.fillText(value, posX - w_offset, posY + h_offset);
}
}
A Pie Chart has an array of segments stored in PieChart.segments, we can look at the startAngle and endAngle of these segments to determine the angle in between where the text would be middleAngle. Then we would move in that direction by Radius/2 to be in the middle point of the chart in radians.
In the example above some other clean-up operations are done, due to the position of text drawn in fillText() being the top right corner, we need to get some offset values to correct for that. And finally textSize is determined based on the size of the chart itself, the larger the chart the larger the text.
Fiddle Example
With some slight modification you can change the discrete number values for a dataset into the percentile numbers in a graph. To do this get the total value of the items in your dataset, call this totalValue. Then on each segment you can find the percent by doing:
Math.round(myPieChart.segments[i].value/totalValue*100)+'%';
The section here myPieChart.segments[i].value/totalValue is what calculates the percent that the segment takes up in the chart. For example if the current segment had a value of 50 and the totalValue was 200. Then the percent that the segment took up would be: 50/200 => 0.25. The rest is to make this look nice. 0.25*100 => 25, then we add a % at the end. For whole number percent tiles I rounded to the nearest integer, although can can lead to problems with accuracy. If we need more accuracy you can use .toFixed(n) to save decimal places. For example we could do this to save a single decimal place when needed:
var value = myPieChart.segments[i].value/totalValue*100;
if(Math.round(value) !== value)
value = (myPieChart.segments[i].value/totalValue*100).toFixed(1);
value = value + '%';
Fiddle Example of percentile with decimals
Fiddle Example of percentile with integers
You can make use of PieceLabel plugin for Chart.js.
{ pieceLabel: { mode: 'percentage', precision: 2 } }
Demo |
Documentation
The plugin appears to have a new location (and name): Demo Docs.
#Hung Tran's answer works perfect. As an improvement, I would suggest not showing values that are 0. Say you have 5 elements and 2 of them are 0 and rest of them have values, the solution above will show 0 and 0%. It is better to filter that out with a not equal to 0 check!
var val = dataset.data[i];
var percent = String(Math.round(val/total*100)) + "%";
if(val != 0) {
ctx.fillText(dataset.data[i], model.x + x, model.y + y);
// Display percent in another line, line break doesn't work for fillText
ctx.fillText(percent, model.x + x, model.y + y + 15);
}
Updated code below:
var data = {
datasets: [{
data: [
11,
16,
7,
3,
14
],
backgroundColor: [
"#FF6384",
"#4BC0C0",
"#FFCE56",
"#E7E9ED",
"#36A2EB"
],
label: 'My dataset' // for legend
}],
labels: [
"Red",
"Green",
"Yellow",
"Grey",
"Blue"
]
};
var pieOptions = {
events: false,
animation: {
duration: 500,
easing: "easeOutQuart",
onComplete: function () {
var ctx = this.chart.ctx;
ctx.font = Chart.helpers.fontString(Chart.defaults.global.defaultFontFamily, 'normal', Chart.defaults.global.defaultFontFamily);
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
this.data.datasets.forEach(function (dataset) {
for (var i = 0; i < dataset.data.length; i++) {
var model = dataset._meta[Object.keys(dataset._meta)[0]].data[i]._model,
total = dataset._meta[Object.keys(dataset._meta)[0]].total,
mid_radius = model.innerRadius + (model.outerRadius - model.innerRadius)/2,
start_angle = model.startAngle,
end_angle = model.endAngle,
mid_angle = start_angle + (end_angle - start_angle)/2;
var x = mid_radius * Math.cos(mid_angle);
var y = mid_radius * Math.sin(mid_angle);
ctx.fillStyle = '#fff';
if (i == 3){ // Darker text color for lighter background
ctx.fillStyle = '#444';
}
var val = dataset.data[i];
var percent = String(Math.round(val/total*100)) + "%";
if(val != 0) {
ctx.fillText(dataset.data[i], model.x + x, model.y + y);
// Display percent in another line, line break doesn't work for fillText
ctx.fillText(percent, model.x + x, model.y + y + 15);
}
}
});
}
}
};
var pieChartCanvas = $("#pieChart");
var pieChart = new Chart(pieChartCanvas, {
type: 'pie', // or doughnut
data: data,
options: pieOptions
});
For Chart.js 3
I've modified "Hung Tran"'s Code.
animation: {
onProgress: function() {
// console.error('this', this);
const ctx = this.ctx;
// ctx.font = Chart.helpers.fontString(Chart.defaults.global.defaultFontFamily, 'normal', Chart.defaults.global.defaultFontFamily);
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
let dataSum = 0;
if(this._sortedMetasets.length > 0 && this._sortedMetasets[0].data.length > 0) {
const dataset = this._sortedMetasets[0].data[0].$context.dataset;
dataSum = dataset.data.reduce((p, c) => p + c, 0);
}
if(dataSum <= 0) return;
this._sortedMetasets.forEach(meta => {
meta.data.forEach(metaData => {
const dataset = metaData.$context.dataset;
const datasetIndex = metaData.$context.dataIndex;
const value = dataset.data[datasetIndex];
const percent = (Math.round(value / dataSum * 1000) / 10) + '%';
const mid_radius = metaData.innerRadius + (metaData.outerRadius - metaData.innerRadius) * 0.7;
const start_angle = metaData.startAngle;
const end_angle = metaData.endAngle;
if(start_angle === end_angle) return; // hidden
const mid_angle = start_angle + (end_angle - start_angle) / 2;
const x = mid_radius * Math.cos(mid_angle);
const y = mid_radius * Math.sin(mid_angle);
ctx.fillStyle = '#fff';
ctx.fillText(percent, metaData.x + x, metaData.y + y + 15);
});
});
}
}
Give the option for pie chart
onAnimationProgress: drawSegmentValues
like:
var pOptions = {
onAnimationProgress: drawSegmentValues
};
var pieChart = new Chart(pieChartCanvas, {
type: 'pie', // or doughnut
data: data,
options: pOptions
});
Easiest way to do this with Chartjs. Just add below line in options:
pieceLabel: {
fontColor: '#000'
}
Best of luck

How to highlight particular region in horizontal way in dygraph and how to create dynamic graph

i need to highlight y value example 20 to -10 and -30 to -45 in y axis. permanently with some color with opacity 50%, how to do.,
in this example how to add external csv file to this following code. Pls Guide me
var orig_range;
window.onload = function(){ var r = [];
var arr = ["7/13/2015 0:15:45",45,"7/13/2015 0:30",5,"7/13/2015 0:45",100,"7/13/2015 1:00",95,"7/13/2015 1:15",88,"7/13/2015 1:30",78];
for (var i = 0; i < arr.length; i++) {
r.push([ new Date(arr[i]),arr[i+1]
]);
i++;
}
orig_range = [ r[0][0].valueOf(), r[r.length - 1][0].valueOf() ];
g2 = new Dygraph(
document.getElementById("div_g"),
r, {
rollPeriod: 7,
animatedZooms: true,
// errorBars: true,
width: 1000,
height: 500,
xlabel: 'date',
ylabel: 'Pressure',
}
);
var desired_range = null;};
function approach_range() {
if (!desired_range) return;
// go halfway there
var range = g2.xAxisRange();
if (Math.abs(desired_range[0] - range[0]) < 60 &&
Math.abs(desired_range[1] - range[1]) < 60) {
g2.updateOptions({dateWindow: desired_range});
// (do not set another timeout.)
} else {
var new_range;
new_range = [0.5 * (desired_range[0] + range[0]),
0.5 * (desired_range[1] + range[1])];
g2.updateOptions({dateWindow: new_range});
animate();
}
}
function animate() {
setTimeout(approach_range, 50);
}
function zoom(res) {
var w = g2.xAxisRange();
desired_range = [ w[0], w[0] + res * 1000 ];
animate();
}
function reset() {
desired_range = orig_range;
animate();
}
function pan(dir) {
var w = g2.xAxisRange();
var scale = w[1] - w[0];
var amount = scale * 0.25 * dir;
desired_range = [ w[0] + amount, w[1] + amount ];
animate();
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/dygraph/1.1.0/dygraph-combined-dev.js"></script>
<div id="div_g"></div>
<div id="output"></div>
<b>Zoom:</b>
hour
day
week
month
full
<b>Pan:</b>
left
right
i'm trying to convert graph to dynamic graph data from csv file
var data = ["te1.csv"];
g2 = new Dygraph(document.getElementById("div_g"), data,
{
drawPoints: true,
showRoller: true,
labels:['date','depth'],
});
setInterval(function() {
data.push([data]);
g2.updateOptions( { 'file': data } );
}, 1000);
i have seen example but i dont know how to link my csv file with dynamic dygraph pls guide me
This example does something extremely similar to what you want: it highlights a specific range on the x-axis. To adapt it, you'd do something like this:
new Dygraph(data, div, {
underlayCallback: function (canvas, area, g) {
var bottom = g.toDomYCoord(highlight_start);
var top = g.toDomYCoord(highlight_end);
canvas.fillStyle = "rgba(255, 255, 102, 1.0)";
canvas.fillRect(area.x, top, area.w, bottom - top);
}
})

Categories