Can you help me how to extend Chart.js v2.0. I need to draw some horizontal lines in the charts, something similar to: http://jsfiddle.net/vsh6tcfd/3/
var originalLineDraw = Chart.controllers.bar.prototype.draw;
Chart.helpers.extend(Chart.controllers.bar.prototype, {
draw: function() {
originalLineDraw.apply(this, arguments);
var chart = this.chart;
var ctx = chart.chart.ctx;
var index = chart.config.data.lineAtIndex;
if (index) {
var xaxis = chart.scales['x-axis-0'];
var yaxis = chart.scales['y-axis-0'];
ctx.save();
ctx.beginPath();
ctx.moveTo(xaxis.getPixelForValue(undefined, index), yaxis.left);
ctx.strokeStyle = '#ff0000';
ctx.lineTo(xaxis.getPixelForValue(undefined, index), yaxis.right);
ctx.stroke();
ctx.restore();
}
}
});
var config = {
type: type,
data: jQuery.extend(true, {}, data),
options: this.chartdata.options,
lineAtIndex: 2
};
new Chart(ctx, config);
Options
With chart.js you have 2 options.
You could create a mix chart types (Example Here). This would allow you to add a line charts to create your lines.
You could create a plugin (See Example Below).
Option 2 would be the one I recommend as it allows you to have more control over the appearance of the lines.
The Fix
demo of the plugin
Chart.js now supports plugins. This allows you to add any features you want to your charts!
To create a plugin you will need to run code after an event has occurred and modify the chart/canvas as needed.
The following code should give you a good starting point:
var horizonalLinePlugin = {
afterDraw: function(chartInstance) {
var yValue;
var yScale = chartInstance.scales["y-axis-0"];
var canvas = chartInstance.chart;
var ctx = canvas.ctx;
var index;
var line;
var style;
if (chartInstance.options.horizontalLine) {
for (index = 0; index < chartInstance.options.horizontalLine.length; index++) {
line = chartInstance.options.horizontalLine[index];
if (!line.style) {
style = "rgba(169,169,169, .6)";
} else {
style = line.style;
}
if (line.y) {
yValue = yScale.getPixelForValue(line.y);
} else {
yValue = 0;
}
ctx.lineWidth = 3;
if (yValue) {
ctx.beginPath();
ctx.moveTo(0, yValue);
ctx.lineTo(canvas.width, yValue);
ctx.strokeStyle = style;
ctx.stroke();
}
if (line.text) {
ctx.fillStyle = style;
ctx.fillText(line.text, 0, yValue + ctx.lineWidth);
}
}
return;
}
}
};
Chart.pluginService.register(horizonalLinePlugin);
Related
trying to extend a chart, so that i can draw lines up to the data point, but this is happening before the default animation. it would look smoother if it applied after.
i have got most of it to work.. but how do i get this to apply after chart animation.
var originalLineDraw = Chart.controllers.line.prototype.draw;
Chart.helpers.extend(Chart.controllers.line.prototype, {
draw: function () {
originalLineDraw.apply(this, arguments);
var chart = this.chart;
var ctx = chart.chart.ctx;
var index = chart.config.data.lineAtIndex;
if (index) {
var xaxis = chart.scales['x-axis-0'];
var yaxis = chart.scales['y-axis-1']
var points = this.chart.getDatasetMeta(this.index).data;
for (var i = 0; i < points.length; i++) {
// var point_x = points[i]._model.x;
var point_y = points[i]._model.y;
ctx.beginPath();
ctx.setLineDash([5, 5]);/*dashes are 5px and spaces are 3px*/
ctx.moveTo(xaxis.getPixelForValue(undefined, i), point_y);
ctx.strokeStyle = '#fff';
ctx.lineTo(xaxis.getPixelForValue(undefined, i), yaxis.bottom);
ctx.stroke();
}
}
}
});
Update...
altho the darw is incorrect it still is playing after animation
var verticalLinePlugin = {
renderVerticalLine: function (chartInstance) {
var chart = chartInstance;
var ctx = chart.chart.ctx;
var maxpoint = [];
//loop the datasets
for (var y = 0; y < chart.config.data.datasets.length; y++) {
var dataset = chart.config.data.datasets[y];
if (dataset.hidden)
continue;
var points = chart.getDatasetMeta(y).data;
for (var i = 0; i < points.length; i++) {
var point_y = points[i]._model.y;
if (point_y < 0)
continue;
var point = maxpoint[i];
if (point == undefined) {
maxpoint.push({ id: i, y: point_y });
} else {
if (point.y > point_y) {
point.y = point_y;
}
}
}
}
var xaxis = chart.scales['x-axis-0'];
var yaxis = chart.scales['y-axis-1']
chart.data.datasets.forEach(function (dataset, i) {
var ds = dataset;
var meta = chart.getDatasetMeta(i);
meta.data.forEach(function (element, index) {
var value = maxpoint[i];
ctx.beginPath();
ctx.setLineDash([5, 5]);
ctx.moveTo(xaxis.getPixelForValue(undefined, i), value.y);
ctx.strokeStyle = '#fff';
ctx.lineTo(xaxis.getPixelForValue(undefined, i), yaxis.bottom);
ctx.stroke();
});
});
},
afterRender: function (chart) {
this.renderVerticalLine(chart);
}
};
Chart.plugins.register(verticalLinePlugin);
I made some small changes (non-intuitive!), and the vertical lines now appear after the animation.
Get the x values from the metadata instead of the data.
Either:
var x_point = element._model.x;
or:
var position = element.tooltipPosition();
var x_point = position.x;
Wrap the drawing in if(!hidden){}, then the vertical lines will disapear and reappear with the data. (The ternary assignment fixes a clash if the data starts hidden)
Do you need the value=max[i]? If just drawing the line up to the points, can get the y_point the same as for x.
var xaxis = chart.scales['x-axis-0'];
var yaxis = chart.scales['y-axis-1'];
chart.data.datasets.forEach(function (dataset, i) {
var meta = chart.getDatasetMeta(i);
var hidden = (meta.hidden != undefined) ? meta.hidden : dataset.hidden
if(!hidden){
meta.data.forEach(function (element, index) {
//var value = maxpoint[i];
var x_point = element._model.x;
var y_point = element._model.y;
ctx.beginPath();
ctx.save();
ctx.setLineDash([5, 5])
ctx.strokeStyle = '#fff';
ctx.moveTo(x_point, y_point); // or value.y
ctx.lineTo(x_point, yaxis.bottom)
ctx.stroke();
ctx.restore();
});
}
});
I have a mixed chart of bar and line graphs. Both are showing their data labels but I only want the bar graph to show its own data labels and not the line graph.
I've got this code from How to show data values or index labels in ChartJs (Latest Version)
How should I tweak it?
animation: {
onComplete: function() {
var ctx = this.chart.ctx;
ctx.font = Chart.helpers.fontString(Chart.defaults.global.defaultFontSize, Chart.defaults.global.defaultFontStyle, Chart.defaults.global.defaultFontFamily);
ctx.fillStyle = "white";
ctx.textAlign = "center";
ctx.textBaseline = "bottom";
this.data.datasets.forEach(function (dataset)
{
for( var i = 0; i < dataset.data.length; i++ )
{
for(var key in dataset._meta)
{
var model = dataset._meta[key].data[i]._model;
ctx.fillText(dataset.data[i], model.x, model.y+15);
}
}
});
}
}
I was able to generate pie chart successfully. However, when I was trying to add a select event listener to the pie chart, it is not triggering the function at all.
function handlePieChartResponse(response)
{
if (response.isError()) {
alert('Error in query: ' + response.getMessage() + ' ' + response.getDetailedMessage());
return;
}
var dataTable = response.getDataTable();
if (dataTable.getNumberOfRows() <= 0) {
document.getElementById('dummyTableRow').innerHTML = '<span>No data found</span>';
return;
}
var chartOptions = DEFAULT_PIE_CHART_OPTIONS;
//var chartOverallPmmLevelCalculated = new google.visualization.ChartWrapper({
// 'chartType': 'PieChart',
// 'containerId': 'chartOverallPmmLevelCalculatedHtml',
// options: chartOptions
//});
var chartRecentPmmLevelCalculated = new google.visualization.PieChart(document.getElementById('chartOverallPmmLevelCalculatedHtml'));
chartRecentPmmLevelCalculated.draw(dataTable, chartOptions);
google.visualization.events.addListener(chartRecentPmmLevelCalculated, 'ready', function () { drawPieChart(); });
google.visualization.events.addListener(chartRecentPmmLevelCalculated, 'select', function () { selectHandler(); });
function drawPieChart() {
var responseDataTable = response.getDataTable();
var chartDataTable = new google.visualization.DataTable();
chartDataTable.addColumn('string', 'LEVEL');
chartDataTable.addColumn('number', 'SCORE');
var chartDataTableRow = new Array();
var rowCounter;
var levelValue;
for (rowCounter = 0; rowCounter < responseDataTable.getNumberOfRows() ; rowCounter++) {
var seek = 0 * 1;
levelValue = responseDataTable.getValue(rowCounter, 0);
chartDataTableRow[seek++] = "LEVEL " + levelValue;
chartDataTableRow[seek++] = responseDataTable.getValue(rowCounter, 1);
chartDataTable.addRow(chartDataTableRow);
}
chartDataTable.sort([{ column: 1 }]);
chartOverallPmmLevelCalculated.setDataTable(chartDataTable);
chartOverallPmmLevelCalculated.draw();
}
handlePieChartResponse.drawPieChart = drawPieChart;
}
function selectHandler() {
alert("This alert triggered from pie chart");
var selectedItem = chartRecentPmmLevelCalculated.getSelection();
if (selectedItem) {
var levelSelected = chartOverallPmmLevelCalculated.getValue(selectedItem.row, 0);
alert(levelSelected);
}
}
I attached 2 images that include before and after the click. I was expecting alert message once select on pie chart slice. But no alert is present and the function call is not triggered.
problem has to do with scope
selectHandler is outside of the function handlePieChartResponse
so it can't be found
just move it inside handlePieChartResponse
then set the event, like so...
...addListener(chartRecentPmmLevelCalculated, 'select', selectHandler);
Within famo.us, I want to place a variable-length wrapping title near the top then have images etc follow. All items must be their own surface as some have click events and animations but all the positioning must be super fast to calculate and place based on the text height and of course must avoid DOM access for the 60fps. This must happen for a series of mini-posts, streaming real time and for infinite scroll.
So far, I came up with an approach that works using an ascii character map to pixel width load off screen on init if isn't already in localStorage. It uses jquery to do the sizing for each character, then determine line height by checking a breaking word and the height of that box. After that I use this map for calculations on the fly so I don't touch DOM again. It seems to work fine but is completely dependent on knowing the style of font going to be used in a very specific way. For each new style, I would have to have a different mapping which sucks.
Is there another approach that is more built in to famo.us?
I'm answering my own question because its frankly been awhile, the new framework is going to come out June 22nd of this month (at the time of the this writing). Just going to put how I'm doing this currently which is the way I described above.
Here's the 0.3.5 way of Famou.us to do this.
This is in a file "utils/lines.js" I whipped up. Hack, yes. Works, yes.
define(function(require) {
'use strict';
var $ = require('jquery');
var cache = require('utils/cache');
function getLineInfo(id) {
var cacheKey = 'lineInfo_' + id;
var savedLineInfo = cache.get(cacheKey);
if (savedLineInfo)
return savedLineInfo;
var charStart = 32; // space
var charEnd = 126; // ~
var $charDump = $('<div>', { id: id });
$('body').append($charDump);
for (var i = charStart; i <= charEnd; i++)
$charDump.append($('<div>', { class: 'char', css: { top: (i-32) * 20 } }).html('&#' + i));
var charMap = {};
$charDump.find('.char').each(function() {
var $this = $(this);
var char = $this.text().charCodeAt(0);
var w = $this.width();
charMap[char] = w;
});
$charDump.html('<div class="line">abc<br>123<br>456</div>');
var lineHeight = $charDump.height() / 3;
$charDump.remove();
savedLineInfo = {
lineHeight: lineHeight,
charMap: charMap
};
cache.set(cacheKey, savedLineInfo);
return savedLineInfo;
}
function getLines(text, width, id, maxLines) {
var lineInfo = getLineInfo(id);
var cleaned = $.trim(text.replace(/\s+/gm, ' '));
var words = cleaned.split(' ');
var lines = 1;
var lineWidth = 0;
var spaceLength = lineInfo.charMap[32];
var allLines = '';
words.forEach(function(word) {
var wordLength = 0;
word.split('').forEach(function(char) {
var charLength = lineInfo.charMap[char.charCodeAt(0)];
if (!charLength)
charLength = spaceLength;
wordLength += charLength;
});
if (lineWidth + wordLength + spaceLength <= width) {
lineWidth += wordLength + spaceLength;
if (maxLines)
allLines += word + ' ';
}
else {
lineWidth = wordLength;
if (maxLines && lines <= maxLines)
allLines += word + ' ';
else if (maxLines)
return {text: allLines.substr(0, allLines.length - 3) + '...', lines: lines};
lines++;
}
});
if (maxLines)
return {text: allLines, lines: lines};
return lines;
}
function getTextHeight(text, width, id, maxLines) {
var lineInfo = getLineInfo(id);
var info;
if (maxLines) {
info = getLines(text, width, id, maxLines);
info.height = lineInfo.lineHeight * info.lines;
return info;
}
return lineInfo.lineHeight * getLines(text, width, id);
}
return {
getLines: getLines,
getLineInfo: getLineInfo,
getTextHeight: getTextHeight
};
});
i want to sort a list of Locations by its Distance(displayed in list).
i already have a code that sould work but since i am that new to the whole mvc thing, i am not really sure where to place it to make it work.
Maybe someone can help me:
var geocoder = new google.maps.Geocoder();
var geo = Ext.create('Ext.util.Geolocation',{
autoUpdate: false,
listeners: {
locationupdate:{
scope: this,
fn: function(geo){
var haversindeDistance = function(lat1,lon1,lat2,lon2){
if(typeof(Number.prototype.toRad)=="undefined"){
Number.prototype.toRad = function(){
return this * Math.PI/180;
}
}
var R = 6371; //km
var dLat = (lat2-lat1).toRad();
var dLon = (lon2-lon1).toRad();
var lat1 = lat1.toRad();
var lat2 = lat2.toRad();
var a = Math.sin(dLat/2)*Math.sin(dLat/2)+
Math.sin(dLong/2)*Math.sin(dLon/2)*Math.cos(lat1)*Math.cos(lat2);
var c = 2*Math.atan2(Math.sqrt(a),Math.sqrt(1-a));
var d = R*c;
// KM or MIles
//return d*0.621371192; //MIles
return d;
};
var store = Ext.getStore('locationsstore');
store.suspendEvents(true);
store.each(function(location){
var lat2 = parseFloat(location.get(geocoder.geocode( { 'address': sAddress}, function(results, status) { })))||0;
var lon2 = parseFloat(location.get(geocoder.geocode( { 'address': sAddress}, function(results, status) { })))||0;
//var lat2 = parseFloat(location.get('lat'))||0;//try to put geocode on this ish
//var lon2 = parseFloat(location.get('lon'))||0;
if(lat2 && lon2){
var distance = haversineDistance(geo.getLatitude(),geo.getLongitude(),lat2,lon2);
location.set('distance',distance);
}
}, this);
store.resumeEvents();
store.filter('distance',/\d/);
store.sort('distance');//check if it is not done or can not be done somewhere else
list.setMasked(false);
}
},
locationerror:{
scope: this,
fn:function(geo,bTimeout,bPermissionDenied,bLocationUnavailable,message){
console.log([geo,bTimeout,bPermissionDenied,bLocationUnavailable,message]);
if(bTimeout){
Ext.Msg.alert('Timed out getting your location.');
}else{
Ext.Msg.alert('Error getting location. Please make sure location services are enabled on your Device.');
}
list.setMask(false);
}
}
}
});
geo.updateLocation();
i found the Solution.Just adding a Listener to the Navigation/List View should do it. I had too much in the Controller so i decided to put it directly into the View.