Chart.JS Vertical Line with Moment.JS Horizontal Axis - chart.js

Is there a way to create vertical lines (event lines, phase changes) using Chart.JS 2.0?
I've seen some examples online (see this related question), HOWEVER, when using Moment.js to create the horizontal axis, it is not possible to give LineAtIndex a moment.js date to create a line at that date.
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-0'];
ctx.save();
ctx.beginPath();
ctx.moveTo(xaxis.getPixelForValue(undefined, index), yaxis.top);
ctx.strokeStyle = '#ff0000';
ctx.lineTo(xaxis.getPixelForValue(undefined, index), yaxis.bottom);
ctx.stroke();
ctx.restore();
}
}
});
Here's a fiddle demonstrating the problem:
https://jsfiddle.net/harblz/0am8vehg/
I believe my issue is that I don't properly understand this bit of code:
var xaxis = chart.scales['x-axis-0'];
var yaxis = chart.scales['y-axis-0'];
If I am able to figure this out, I'll post a working fiddle here for any future users tackling the same project.
Thanks you for your time reading this :)

I tried multiple plugins, but none could handle Charts with cartesian axes of type time. My rather simple solution:
First register the chart plugin globally:
Chart.plugins.register({
drawLine: function (chart, xValue, color = 'rgba(87,86,86,0.2)') {
const canvas = chart.chart
const context = canvas.ctx
context.beginPath()
context.moveTo(xValue, 6) // top
context.lineTo(xValue, canvas.height - 73) // bottom
context.strokeStyle = color
context.stroke()
},
afterDraw: function (chart) {
const xScale = chart.scales['x-axis-0']
if (chart.options.verticalLine) {
chart.options.verticalLine.forEach((line) => {
const xValue = xScale.getPixelForValue(line)
if (xValue) {
this.drawLine(chart, xValue)
}
})
}
}
})
Then add verticalLine Array to your chart definition:
options: {
scales: { xAxes: [{ type: 'time' }] },
verticalLine: ['2019-04-1', '2019-07-01', '2019-10-01'],
}

Related

ChartJs: Is there a way to control the font options per line for a multiline axis label

I am open to learning that there is already a way (via configuration, or developing a plugin) to hook into the rendering of the label of an axis, such that I could control aspects of the font used to render each line of a multiline label (e.g., what I need to render would be similar visually to a label and sub-label below it, with the primary label being bolded and a larger font size, while the sub-label directly beneath it would be normal font weight and a smaller size).
I am using ChartJs version 3.5.1 to render a horizontal barchart (meaning that the dataset labels on the left are really configured under the y axis), and have tried a few different things already:
Hooking into the tick callback - but I can't even use this function to duplicate default functionality (the value coming into that function isn't the label text; instead it is the index/ordinal of the data row?). Even if I could get this to work as shown in examples, it appears like this would be more for the content of the label than any of the configuration options themselves.
Setting the font configuration for ticks to be an array - but this only serves to allow me to change the font between data rows (e.g., I can make the label of the top row in my horizontal bar chart be size 22, the second label 10, etc. - but not change font attributes within lines of a given label)
Using a plugin like afterDraw to try to go tweak things - but again, the configuration at that point seems to only consider all of the lines together as one label.
Tried looking through past PRs to the project (mostly centered around adding multiline label support, as well as bug fixes specific to that area) to get any additional insight
If there isn't a way currently (via plugins or existing configuration), does anyone have a good feel for where to start attacking this sort of a change as a new PR?
UPDATE
As was shared as a response to my corresponding ChartJs feature request and as the accepted answer below, a custom plugin seems to be the only way currently to accomplish what I wanted for now.
Here are the key bits from my configuration (admittedly much more "one time use only" than the accepted answer, as I moved some of the configuration inside of the plugin as hard-coded values given my relatively narrow use case):
// this will be passed into the chart constructor...
const options = {
//...
scales: {
//...
// I wanted to impact the lefthand side of a horizontal bar chart
y: {
ticks: {
// make the original labels white for later painting over with custom sub-labels
color: "white",
// we still want this here to be able to take up the same space as the eventual label we will stick here
font: {
size: 22,
weight: "bold"
}
}
},
//...
}
};
// This is my plugin, also later passed into the chart constructor
const customSubLabelsPlugin = {
id: "customSubLabels",
afterDraw: (chart, args, opts) => {
// Set all variables needed
const {
ctx,
// I only cared about altering one specific axis
scales: { y }
} = chart;
const labelItems = y._labelItems;
const fontStringSubTitle = "16px Helvetica,Arial,sans-serif";
const fontStringMain = "bold 22px Helvetica,Arial,sans-serif";
// loop over each dataset label
for (let i = 0; i < labelItems.length; i++) {
let labelItem = labelItems[i];
// For purposes of redrawing, we are going to always assume that each label is an array - because we make it that way if we need to
const label = Array.isArray(labelItem.label)
? labelItem.label
: [labelItem.label];
// Draw new text on canvas
let offset = 0;
label.forEach((el) => {
let elTextMetrics = ctx.measureText(el);
if (labelItem.label.indexOf(el) === 0) {
ctx.font = fontStringMain;
} else {
ctx.font = fontStringSubTitle;
}
ctx.save();
ctx.fillStyle = "#546a6f";
ctx.fillText(
el,
labelItem.translation[0],
labelItem.translation[1] + labelItem.textOffset + offset
);
ctx.restore();
offset +=
elTextMetrics.fontBoundingBoxAscent +
elTextMetrics.fontBoundingBoxDescent;
});
}
}
};
You can use a plugin to redraw the ticks for you, might need some finetuning for your specific needs:
var options = {
type: 'line',
data: {
labels: [
["Red", "subTitle"],
["Blue", "subTitle"],
["Yellow", "subTitle"],
["Green", "subTitle"],
["Purple", "subTitle"],
["Orange", "subTitle"]
],
datasets: [{
label: '# of Votes',
data: [12, 19, 3, 5, 2, 3],
borderColor: 'red',
backgroundColor: 'red'
}]
},
options: {
plugins: {
customTextColor: {
color: 'blue',
boxColor: 'white',
fontStringSubTitle: 'italic 12px Comic Sans MS',
fontStringMain: ''
}
}
},
plugins: [{
id: 'customTextColor',
afterDraw: (chart, args, opts) => {
// Set all variables needed
const {
ctx,
scales: {
y,
x
}
} = chart;
const labelItems = x._labelItems;
const {
color,
boxColor,
fontStringMain,
fontStringSubTitle
} = opts;
const defaultFontString = '12px "Helvetica Neue", Helvetica, Arial, sans-serif';
for (let i = 0; i < labelItems.length; i++) {
let labelItem = labelItems[i];
if (!Array.isArray(labelItem.label)) {
continue;
}
let metrics = ctx.measureText(labelItem.label);
let labelWidth = metrics.width;
let labelHeight = metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent;
//Draw box over old labels so they are inviseble
ctx.save();
ctx.fillStyle = boxColor || '#FFFFFF';
ctx.fillRect((labelItem.translation[0] - labelWidth / 2), labelItem.translation[1], labelWidth, labelHeight * labelItem.label.length);
ctx.restore();
// Draw new text on canvas
let offset = 0;
labelItem.label.forEach(el => {
let elTextMetrics = ctx.measureText(el);
let elWidth = elTextMetrics.width;
if (labelItem.label.indexOf(el) === 0) {
ctx.font = fontStringMain || defaultFontString;
} else {
ctx.font = fontStringSubTitle || defaultFontString;
}
ctx.save();
ctx.fillStyle = color || Chart.defaults.color
ctx.fillText(el, (labelItem.translation[0] - elWidth / 2), labelItem.translation[1] + labelItem.textOffset + offset);
ctx.restore();
offset += elTextMetrics.fontBoundingBoxAscent + elTextMetrics.fontBoundingBoxDescent;
});
}
// Draw white box over old label
}
}]
}
var ctx = document.getElementById('chartJSContainer').getContext('2d');
new Chart(ctx, options);
<body>
<canvas id="chartJSContainer" width="600" height="400"></canvas>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/3.5.1/chart.js"></script>
</body>

Vertical scrubber line using chartjs

Is there a way to get a vertical scrubber line when hovering over a line chart using chart.js v2? Similar to the way a vertical line appears using this rickshaw example: http://code.shutterstock.com/rickshaw/examples/formatter.html
I can use tool tips on the actual points along the line, but it'd like to be able to have a vertical line appear as the user hovers over the chart and scrubs left to right
You can extend Chart.js to do this. Just override the showTooltip method after initializing the chart.
Preview
Script
Chart.types.Line.extend({
name: "LineAlt",
initialize: function () {
Chart.types.Line.prototype.initialize.apply(this, arguments);
var originalShowTooltip = this.showTooltip;
this.showTooltip = function (activePoints) {
if (activePoints.length) {
var ctx = this.chart.ctx;
var scale = this.scale;
ctx.save();
ctx.strokeStyle = '#aaa';
ctx.beginPath();
ctx.moveTo(activePoints[0].x, scale.startPoint);
ctx.lineTo(activePoints[0].x, scale.endPoint);
ctx.stroke();
ctx.restore();
}
return originalShowTooltip.apply(this, arguments);
}
}
});
and then
new Chart(ctx).LineAlt(data);
Fiddle - http://jsfiddle.net/98gz1fhw/

Chart.js how to rewrite x for webAPP

I use chart.js for a webApp, but it is not I want.
How to rewrite this x axle or I should change other js to draw chart,demo:
You could extend the chart to remove the points that you don't need from the x-axis, like so (adapted from https://stackoverflow.com/a/31606933/360067)
Chart.types.Line.extend({
name: "LineAlt",
initialize: function (data) {
Chart.types.Line.prototype.initialize.apply(this, arguments);
if (this.options.every) {
var every = this.options.every;
var xLabels = this.scale.xLabels
xLabels.forEach(function (label, i) {
if (i % every !== 0)
xLabels[i] = '';
})
}
}
});
and you call it like so
var ctx = document.getElementById("chart").getContext("2d");
var myLineChart = new Chart(ctx).LineAlt(data, {
every: 3
});
Adjust every to 2, 3,... to show one in every 2, every 3,... points.
Fiddle - http://jsfiddle.net/3p2ekjyn/

How to implement Slide to delete in Famo.us with proper event handling

I'm trying to implement a slide to delete. As part of that I have a layer with opacity set to 0 the idea being I'm trying to set several if clauses to gradual change the opacity of the surface so that the word Delete gentle appears as you slide it to the left. At this point I just have it switching at 10pixels for testing. The functions fire but the opacity doesn't change. I think it has something to do with not being piped/event handling being done properly on my part. Any Ideas?
var SnapTransition = require("famous/transitions/SnapTransition");
Transitionable.registerMethod('snap', SnapTransition);
var CSS = require("css/recentActivityCSS");
var Ctrl = require("controllers/recentActivityCtrl");
var homeContentWrap = new Scrollview();
var recentActivities = [];
var ContainerSize = [undefined, 100];
homeContentWrap.sequenceFrom(recentActivities);
for (var i = 0; i < Ctrl.recentActivityList.length; i++) {
var recentActivitiesContainer = new ContainerSurface({
size: ContainerSize,
properties: CSS.recentActivitiesContainer,
});
var redLayer = new Surface({
size: ContainerSize,
content: 'DELETE',
properties: CSS.redLayer,
});
var draggable = new Draggable({
xRange: [-120, 5],
yRange: [0, 0],
});
var textContainer = new ContainerSurface({
size: ContainerSize,
properties: CSS.textContainer,
});
var mod = new Modifier({});
node = new RenderNode(draggable);
node.add(mod).add(textContainer);
textContainer.pipe(draggable);
textContainer.pipe(homeContentWrap);
var opacityMod = new StateModifier({
opacity: 0
});
recentActivitiesContainer.add(node);
recentActivitiesContainer.add(opacityMod).add(redLayer);
recentActivities.push(recentActivitiesContainer);
var trans = {
method: 'snap',
period: 100,
dampingRatio: 0.3,
velocity: 5
};
draggable.on('start', function() {});
draggable.on('update', function() {
var position = this.getPosition();
if (position[0] > (-10)) {
opacityMod.halt();
opacityMod.setOpacity(0, { curve: 'easeOut', duration: 10 });
} else {
opacityMod.halt();
opacityMod.setOpacity(1, { curve: 'easeOut', duration: 10 });
}
});
draggable.on('end', function(){
var position = this.getPosition();
if (position[0] < (-100)) {
alert('delete');
}
this.setPosition([0,0,0], trans);
});
There are a couple of things I did to the draggable 'update' function to achieve what you have described.
1) You need to bind objects to your update function or else you have no real reference to them. When you use opacityMod in your 'update' function, you only alter the last cells opacityMod. Since binding will change the meaning of 'this', I also bind draggable.
2) You say you want a gradual fade. This approach is not going to give you anything gradual. You need to take the position of the draggable and calculate an opacity based on that value. To start, I declare two new variables for fadeStart and fadeEnd, that represent the positions of the draggable X position 0 and 1 opacity respectively.
Also you probably do not need the transition in your setOpacity, but I kept it in anyway.
Here is the updated 'update' function.. Good Luck!
fadeStart = -10;
fadeEnd = -100;
draggable.on('update', function() {
var draggable = this[0];
var opacityMod = this[1];
var position = draggable.getPosition();
if ( position[0] > fadeStart ) {
opacityMod.halt();
opacityMod.setOpacity(0, { curve: 'easeOut', duration: 10 });
} else if ( position[0] > fadeEnd ) {
opacity = (position[0] - fadeStart) / ( fadeEnd - fadeStart );
opacityMod.halt();
opacityMod.setOpacity(opacity, { curve: 'easeOut', duration: 10 });
} else {
opacityMod.halt();
opacityMod.setOpacity(1, { curve: 'easeOut', duration: 10 });
}
}.bind([draggable,opacityMod]));
John has clearly answered this question above, but I wanted to show an alternate approach to the problem. I've seen questions here and in the #famous irc where people are having eventing problems. I've also seen a number of people struggling with binding or the lack of it. And finally, if you work out the whole slide thing here, shouldn't you be able to put that behind you and simply drop it in elsewhere? With that in mind I wrote a program that simply puts several images into a scrollview. Then I wrote a function called createSlidePanel that encapsulated the slide functionality and then to enable the fade-in of the word "delete" I created a second helper function createModifyingView. This approach appears to hit all three points above. I broke the eventing problems down into smaller more manageable units. It completely eliminated the need for this and binding. And finally, the two helper functions can be reused.
Here is my version of "main.js" which contains fundamentally "application" behavior:
/* globals define */
define(function(require, exports, module) {
'use strict';
// import dependencies
var Engine = require('famous/core/Engine');
var ImageSurface = require('famous/surfaces/ImageSurface');
var Surface = require('famous/core/Surface');
var Scrollview = require('famous/views/Scrollview');
var SnapTransition = require('famous/transitions/SnapTransition');
var Transitionable = require('famous/transitions/Transitionable');
var createSlidePanel = require('SlidePanel');
var createModifyingView = require('ModifyingView');
Transitionable.registerMethod('snap', SnapTransition);
var trans = {
method: 'snap',
period: 100,
dampingRatio: 0.3,
velocity: 5
};
var dataSource = [
'http://www.outerspaceuniverse.org/wp-content/uploads/2009/07/outer-space1.jpg',
'http://wallpoper.com/images/00/39/95/84/outer-space_00399584.jpg',
'http://static1.businessinsider.com/image/508c649e69beddb270000005/the-only-reason-private-space-flight-isnt-laughed-at-is-nasas-11-billion-infusion.jpg'
];
var images = [];
var slideOptions = {
drag: {
xRange: [-120, 5],
projection: 'x',
},
view: {
size:[300,300]
}
};
var mainContext = Engine.createContext();
var scrollView = new Scrollview();
mainContext.add(scrollView);
dataSource.forEach(function(url,i,urls) {
var img = new ImageSurface({
content: url,
size: [300,300]
});
var dlt = new Surface({
size:[300,300],
content: 'DELETE',
properties: {
color: 'red',
zIndex: 4,
lineHeight: '200px',
fontSize:'60px'
}
});
var modView = createModifyingView();
modView.modifier.setOpacity(0);
modView.add(dlt);
var elem = createSlidePanel(slideOptions);
elem.addSlide(img)
elem.addStill(modView);
elem._eventOutput.pipe(scrollView);
elem.on('slideupdate',slideUpdateHandler);
elem.on('slideend',slideEndHandler);
images.push(elem);
});
function slideUpdateHandler(eventInfo) {
var ratio = (eventInfo.data.position[0]-slideOptions.drag.xRange[1])/(slideOptions.drag.xRange[0]-slideOptions.drag.xRange[1]);
if(ratio>.2) {
eventInfo.source.stillElements[0].modifier.setOpacity(ratio);
} else {
eventInfo.source.stillElements[0].modifier.setOpacity(0);
}
}
function slideEndHandler(eventInfo) {
if (eventInfo.data.position[0] < (-100)) {
alert('delete');
}
eventInfo.source.modifier.setPosition([0,0,0], trans);
eventInfo.source.stillElements[0].modifier.setOpacity(0);
}
scrollView.sequenceFrom(images);
});
The slide functionality is here in "SlidePanel.js":
/* globals define */
define(function(require, exports, module) {
'use strict';
// import dependencies
var Modifier = require('famous/core/Modifier');
var View = require('famous/core/View');
var Draggable = require('famous/modifiers/Draggable');
function createSlidePanel(options) {
options = options || {};
var slidePanel = new View(options.view);
slidePanel.slideElements = [];
slidePanel.stillElements = [];
slidePanel.modifier = new Draggable(options.drag);
var node = slidePanel._add(slidePanel.modifier);
slidePanel.addSlide = function addSlide(renderable) {
node.add(renderable);
renderable.pipe(slidePanel._eventOutput);
renderable.pipe(slidePanel.modifier);
slidePanel.slideElements.push(renderable);
}
slidePanel.addStill = function addStill(renderable) {
slidePanel.add(renderable);
renderable.pipe(slidePanel._eventOutput);
renderable.pipe(slidePanel.modifier);
slidePanel.stillElements.push(renderable);
}
slidePanel.modifier.on('start',function(data) {
slidePanel._eventOutput.emit('slidestart',{source:slidePanel,data:data});
});
slidePanel.modifier.on('update',function(data) {
slidePanel._eventOutput.emit('slideupdate',{source:slidePanel,data:data});
});
slidePanel.modifier.on('end',function(data) {
slidePanel._eventOutput.emit('slideend',{source:slidePanel,data:data});
});
slidePanel.modifier.activate();
return slidePanel;
}
module.exports = createSlidePanel;
});
And here is the "ModifyingView.js" code:
/* globals define */
define(function(require, exports, module) {
'use strict';
// import dependencies
var Modifier = require('famous/core/Modifier');
var View = require('famous/core/View');
function createModifyingView(options) {
options = options || {};
var view = new View(options);
view.modifier = new Modifier();
var node = view._add(view.modifier);
view.add = function add(renderable) {
node.add(renderable);
view._eventOutput.subscribe(renderable);
};
view.setPosition = function setPosition(/* passthrough */) {
view.modifier.setPosition(arguments);
};
view.setOpacity = function setOpacity(/* passthrough */) {
view.modifier.setOpacity(arguments)
}
//view.modifier.setPosition([0,0,0]);
return view;
}
module.exports = createModifyingView;
});
Several Notes:
Obviously, one of the main changes here is the functional pattern which makes all references explicit and leaves no question of binding.
Yes this is more code than the original, partly because it is complete with all of the require statements and the list of images, but also because it just is. The trade-off here is that you may get more bang for the buck if you reuse the helpers.
The ModifyingView pattern is one I use quite a bit. This comes up so often whether I'm building a login form with eight surfaces interacting in ways the main program need know nothing about, or a simple surface fading in and out, that I have a code snippet which defines a view, a modifier, a statemodifier (one of which I usually delete,) a surface and much of the common code to tie them together.
I'm specifically not recommending the "options" management used in this code, but it suffices for the example.

Automatically display legends in Google visualization charts

I have a sample Google visualization dashboard in this fiddle in which the chart is drawn as,
Chart = new google.visualization.ChartWrapper({
'chartType': 'ColumnChart',
'containerId': 'chart1',
'options': {
'width': 600,
'height': 180,
'isStacked': true,
'legend': 'top',
}
});
For CPU in control picker, there is only value1. But the chart legend shows both. How can I hide the legend value2 if it has 0 and vice versa.
Every time the state changes on the control, you need to get the data used by the chart and parse it to determine whether or not there are non-0 values in the selected range for each data series. If there are non-0 values, add the data series to the chart's view.columns parameter, otherwise leave it out:
google.visualization.events.addListener(categoryPicker, 'statechange', function () {
google.visualization.events.addOneTimeListener(Chart, 'ready', function () {
var cols = [0];
var dt = Chart.getDataTable();
for (var i = 1; i < dt.getNumberOfColumns(); i++) {
var range = dt.getColumnRange(i);
console.dir(range);
// assumes there are no null values
if (range.min !== 0 || range.max !== 0) {
cols.push(i);
}
}
var view = Chart.getView() || {};
view.columns = cols;
Chart.setView(view);
Chart.draw();
});
});
fiddle