I am trying to create a line chart plugin that will draw reference letters under some points. To do so, the plugin uses a custom afterDatasetsDraw function to perform the drawing. However, I am unable to find a way to pass in the reference letters for the desired points. Below is an example of what I'm trying to achieve with the red circled letters.
Does anyone have an idea on how to pass in the reference letters for the corresponding points?
Thanks.
I would just define some configuration properties for your new plugin and use one of those properties to define where the point reference should be located and what the reference value should be.
Here is an example of what I mean. This would be in the chart's options property.
pointReferenceLetters: {
display: true,
fontColor: 'green',
references: [
{datasetIndex: 0, dataIndex: 1, reference: 'A'},
{datasetIndex: 1, dataIndex: 2, reference: 'B'},
]
}
The plugin would then use this data to draw the point references. Here is an example showing how a plugin would use this data. Note, I just did a quick implementation to show the concept, this plugin does not draw the reference circle like yours would.
Chart.plugins.register({
afterDraw: function(chartInstance) {
if (chartInstance.config.options.pointReferenceLetters || chartInstance.config.options.pointReferenceLetters.display) {
var references = chartInstance.config.options.pointReferenceLetters.references || [];
var helpers = Chart.helpers;
var ctx = chartInstance.chart.ctx;
var fontColor = helpers.getValueOrDefault(chartInstance.config.options.pointReferenceLetters.fontColor, chartInstance.config.options.defaultFontColor);
// render the value of the chart above the bar
ctx.font = Chart.helpers.fontString(Chart.defaults.global.defaultFontSize + 5, 'normal', Chart.defaults.global.defaultFontFamily);
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
ctx.fillStyle = fontColor;
chartInstance.data.datasets.forEach(function (dataset, dsindex) {
for (var i = 0; i < dataset.data.length; i++) {
// note, many browsers don't support the array.find() function.
// if you use this then be sure to provide a pollyfill
var refPoint = references.find(function(e) {
return e.datasetIndex == dsindex && e.dataIndex === i
});
if (refPoint) {
var model = dataset._meta[Object.keys(dataset._meta)[0]].data[i]._model;
ctx.fillText(refPoint.reference, model.x, model.y + 30);
}
}
});
}
}
});
As you an see, the plugin uses the data provided in the pointReferenceLetters.references property to determine when a point reference should be drawn and then uses the values provided as the reference text.
Here is a codepen example that demonstrates all of this.
Related
I need to add more space above and below chart area (near top and bottom scale).
It seems that only ability to add a padding to the vertical axes.
I disabled tick lines according to documentation:
http://www.chartjs.org/docs/#scales:
Chart.defaults.scale.gridLines.drawTicks = false;
chartjs chart area top and bottom paddings image:
Also I can add padding to vertical axes scale labels (ticks)
Chart.defaults.scale.ticks.padding = 15;
How can I add a padding above a top scale and below a bottom (zero) scale?
There are a few ways to control padding between scales/legends in chart.js (some official ways documented in the docs and some "hacky" ways described elsewhere). The problem is, there just isn't a way to use the existing configuration options to control padding through out the chart (left scale, bottom scale, top scale...or bottom of legend, etc.).
Fortunately, because of the flexible chart.js interfaces (and because we can create new scale types, etc.), it is still possible to control the padding without too much fuss. Let me explain the way to add left, top, and bottom padding and then provide a working example at the end very end (skip ahead if you so desire).
Left Padding
This one is easy. There is a documented option to control this. Just set the scales.yAxes.ticks.padding option to whatever value you want. Here is an example.
scales: {
yAxes: [{
ticks: {
beginAtZero: true,
padding: 25,
}
}]
}
Top Padding (or Legend Padding)
There is no option to control this so we have to build it in. I built it in by creating a new Legend object and overwriting the afterFit() function that uses a paddingBottom option set on the options object. This isn't too difficult but requires a round around way to do it. Here is the relevant code.
function getBoxWidth(labelOpts, fontSize) {
return labelOpts.usePointStyle ?
fontSize * Math.SQRT2 :
labelOpts.boxWidth;
};
Chart.NewLegend = Chart.Legend.extend({
afterFit: function() {
this.height = this.height + this.options.paddingBottom;
},
});
function createNewLegendAndAttach(chartInstance, legendOpts) {
var legend = new Chart.NewLegend({
ctx: chartInstance.chart.ctx,
options: legendOpts,
chart: chartInstance
});
if (chartInstance.legend) {
Chart.layoutService.removeBox(chartInstance, chartInstance.legend);
delete chartInstance.newLegend;
}
chartInstance.newLegend = legend;
Chart.layoutService.addBox(chartInstance, legend);
}
// Register the legend plugin
Chart.plugins.register({
beforeInit: function(chartInstance) {
var legendOpts = chartInstance.options.legend;
if (legendOpts) {
createNewLegendAndAttach(chartInstance, legendOpts);
}
},
beforeUpdate: function(chartInstance) {
var legendOpts = chartInstance.options.legend;
if (legendOpts) {
legendOpts = Chart.helpers.configMerge(Chart.defaults.global.legend, legendOpts);
if (chartInstance.newLegend) {
chartInstance.newLegend.options = legendOpts;
} else {
createNewLegendAndAttach(chartInstance, legendOpts);
}
} else {
Chart.layoutService.removeBox(chartInstance, chartInstance.newLegend);
delete chartInstance.newLegend;
}
},
afterEvent: function(chartInstance, e) {
var legend = chartInstance.newLegend;
if (legend) {
legend.handleEvent(e);
}
}
});
Bottom Padding
There is also no option to control this, so we have to also build it in. Since we are dealing with a scale here, the best way to do this is extending the 'category' scale and add logic to handle a scale paddingTop option. After reading through the source, we need to overwrite the draw() function to do this. Here is the relevant code (see my example for the full implementation).
// ...
if (isHorizontal) {
if (options.position === 'bottom') {
// bottom
textBaseline = !isRotated? 'top':'middle';
textAlign = !isRotated? 'center': 'right';
labelY = me.top + tl + me.options.paddingTop;
} else {
// top
textBaseline = !isRotated? 'bottom':'middle';
textAlign = !isRotated? 'center': 'left';
labelY = me.bottom - tl;
}
}
// ...
Here is a codepen example showing all this put together.
Edit: This does not provide provide padding inside the chart (which is what this question is originally about), but rather adds a padding around the chart
For those still looking for this that might end up here in a google search (like me), it seems to have been solved in a later version: https://www.chartjs.org/docs/latest/configuration/layout.html#padding
let chart = new Chart(ctx, {
type: 'line',
data: data,
options: {
layout: {
padding: {
left: 50,
right: 0,
top: 0,
bottom: 0
}
}
}
});
For ChartJs version 3 you can remove tickLenght:
scales: {
x: {
grid: {
tickWidth:0,
tickLength: 0,
},
y: {
grid: {
tickWidth:0,
tickLength: 0,
},
...
}
This does not add padding between the chart and legend but the above code no longer seems to work on Chart.js v3 and above. Here is a workaround plugin that pads the y-axis to ensure labels (only checked with chartjs-plugin-datalabels) fall within the chart.
var pluginAutoAdjustRangeY = {
id: 'autoAdjustRangeY',
beforeInit: function(chartInstance) {
var legendOpts = chartInstance.options.legend;
if (legendOpts) {
chartInstance.legend.afterFit = function(){
var max = 0;
var min = 0;
var datasets = chartInstance.data.datasets.length
for(let i = 0; i < datasets; i++){
var points = chartInstance.data.datasets[i].data.length
for(let p = 0; p < points; p++){
var v = parseFloat(chartInstance.data.datasets[i].data[p]);
if(v > max) max = v;
if(v < min) min = v;
}
}
var range = parseInt((max - min) * ((chartInstance.options.legend.padding) || 0));
chartInstance.options.scales.y.max = parseInt(max + range);
if(min !== 0) chartInstance.options.scales.y.min = parseInt(min - range);
}
}
},
beforeLayout: function(chartInstance) { //2
if(chartInstance.options.legend){
if(chartInstance.legend.afterFit){
chartInstance.legend.afterFit();
}
}
},
};
And can be used by as such:
options: {
legend: {
padding: 0.15, //percentage of Y range to pad ends of axis
}
}
I use a line chart rendered by Chart.js. I use multiple datasets whereas dataset consists of one data point (x,y) only (given from the business domain).
The chart displays the information about each point (x,y,label) when hovering it, however I'd like to have a label next to each point by default/everytime that shows the respective label.
I could not find any solution during my research.
Any pointers? Is that actually possible to achieve with Chart.js?
It is possible to do this with chart.js using the plugins API. Here is an example of a plugin that I use in production to show labels for all data points when the chart renders.
Note, I currently only use bar charts so you might have to tweak some things for line charts. As you will also see, I added a new options property called showDatapoints that I can set to true in order to use the plugin on certain graphs.
Chart.plugins.register({
afterDraw: function(chartInstance) {
if (chartInstance.config.options.showDatapoints) {
var helpers = Chart.helpers;
var ctx = chartInstance.chart.ctx;
var fontColor = helpers.getValueOrDefault(chartInstance.config.options.showDatapoints.fontColor, chartInstance.config.options.defaultFontColor);
// render the value of the chart above the bar
ctx.font = Chart.helpers.fontString(Chart.defaults.global.defaultFontSize, 'normal', Chart.defaults.global.defaultFontFamily);
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
ctx.fillStyle = fontColor;
chartInstance.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;
var scaleMax = dataset._meta[Object.keys(dataset._meta)[0]].data[i]._yScale.maxHeight;
var yPos = (scaleMax - model.y) / scaleMax >= 0.93 ? model.y + 20 : model.y - 5;
ctx.fillText(dataset.data[i], model.x, yPos);
}
});
}
}
});
I'm working with Chart.js and I'm wondering if there's a way when you click on part of a pie chart, it filters the bar chart.
Since this is a Chart.js question :-), this is how you do it Chart.js (and it's not too complex either)
Setting up the Pie Chart
// pie
var data = [
{
value: 300,
color: "#F7464A",
highlight: "#FF5A5E",
label: "Red",
subData: [28, 48, 40, 19, 86, 27, 190]
}, {
value: 50,
color: "#46BFBD",
highlight: "#5AD3D1",
label: "Green",
subData: [90, 28, 48, 40, 19, 86, 127]
}, {
value: 100,
color: "#FDB45C",
highlight: "#FFC870",
label: "Yellow",
subData: [28, 48, 40, 19, 86, 27, 190]
}
]
var canvas = document.getElementById("chart");
var ctx = canvas.getContext("2d");
var myPieChart = new Chart(ctx).Pie(data);
Setting up the Bar Chart using Pie Data
// bar using pie's sub data
var bardata = {
labels: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"],
datasets: [
{
label: "My Second dataset",
fillColor: "rgba(151,187,205,0.5)",
strokeColor: "rgba(151,187,205,0.8)",
highlightFill: "rgba(151,187,205,0.75)",
highlightStroke: "rgba(151,187,205,1)",
data: data[0].subData.map(function (point, i) {
var pointTotal = 0;
data.forEach(function (point) {
pointTotal += point.subData[i]
})
return pointTotal;
})
}
]
};
var subcanvas = document.getElementById("subchart")
var subctx = subcanvas.getContext("2d");
var myBarChart = new Chart(subctx).Bar(bardata);
Updating Bar data when Clicking Pie
// connect them both
canvas.onclick = function (evt) {
var activeSector = myPieChart.getSegmentsAtEvent(evt);
myBarChart.datasets[0].bars.forEach(function (bar, i) {
var pointTotal = 0;
data.forEach(function (point, j) {
if (activeSector.length === 0 || point.label === activeSector[0].label)
pointTotal += data[j].subData[i]
})
bar.value = pointTotal;
});
myBarChart.update();
};
Clicking outside the pie (but in the pie chart's canvas) resets the bar chart.
Fiddle - http://jsfiddle.net/0zwkjv8a/
Other answers posted already cover what I would generally advise here which is to use dc-js if you want crossfilter enabled charts out of the gate. I would have commented on this answer, but I don't have enough reputation so I'm posting this as option 'c.)' where 'a.)' is using dc-js and 'b.)' is making some modifications to an existing Chart.js chart.
Option 'c.)' is to extend the Chart.js chart type and make the child chart work like a dc-js chart. Chart.js chart types follow an inheritance hierarchy, so if you like a chart that already exists you can wrap its prototype methods with some of your own. Additionally important to this option, in the selected answer to the stack overflow question with heading 'dc.js - Listening for chart group render', it is described how the current implementation of dc-js's chartRegistry object is fairly decoupled from d3 or dc internals, so any chart implementing chartRegistry's interface can be part of a chartGroup.
I was in the position of wanting very much to use Polar Area Charts in a dataset where I was already using a chart group full of dc-js charts to crossfilter the data. I wrote an extension for Polar Area charts that could serve as an example of one way (I'm going to go ahead and say probably not the best way) to extend a chart type with dc-js like behaviors. The repo for this is at https://github.com/nsubordin81/Chart.dc.js, Licensed under an MIT License, and in case that ever goes anywhere, all of the code is copied into the example fiddle: http://jsfiddle.net/nsubordin81/3w725o3c/1/
Chart.dc.js v. 0.1.0
MIT Licensed: opensource.org/licenses/MIT
Copyright (c) 2015 Taylor Bird
(function () {
"use strict";
var root = this,
Chart = root.Chart,
dc = root.dc,
helpers = Chart.helpers,
//class for data structure that manages filters as they relate to chart segments. This should probably be generalized to chart elements of all kinds.
FilterManager = function (segmentList) {
//private member variable
var filterMap = [];
//constructor
//accepts a list of SegmentArcs that have had the extra properties added to them
for (var i = 0; i < segmentList.length; i++) {
add(segmentList[i].segmentID);
}
//private methods
function testOnAll(test) {
var testResult = true;
for (var i = 0; i < filterMap.length; i++) {
//one failure of test means testOnAll fails
if (!test(filterMap[i])) {
testResult = false;
}
}
return testResult;
}
//add a filter, pretty much just a wrapper for push
function add(segmentID) {
filterMap.push({
"segmentID": segmentID,
"active": false
});
}
//remove a filter by id, returns removed filter
function remove(segmentID) {
var removed = filterMap.find(segmentID);
filterMap = filterMap.filter(function (elem) {
return elem.segmentID !== segmentID;
});
return removed;
}
//return this segment if it is filtered
function find(segmentID) {
for (var i = 0; i < filterMap.length; i++) {
if (filterMap[i].segmentID === segmentID) {
return filterMap[i];
}
}
return -1;
}
//public methods
return {
//tell me if the filter for this segment is active
isActive: function (segmentID) {
var filter = find(segmentID);
if (filter === -1) {
console.error("something went wrong, the filter for this segment does not exist");
}
return filter.active;
},
//for the given segment, activate or deactivate its filter. return whether the filter is on or off.
flip: function (segmentID) {
var filter = find(segmentID);
if (filter === -1) {
console.error("something went wrong, the filter for this segment does not exist");
}
filter.active ? filter.active = false : filter.active = true;
return filter.active;
},
//if all filters are on, we want to be able to quickly deactivate them all
turnAllOff: function () {
for (var i = 0; i < filterMap.length; i++) {
filterMap[i].active = false;
}
},
//tell me if all of the filters are off
allOff: function () {
return testOnAll(function (elem) {
return !elem.active;
});
},
//tell me if all the filters are on
allOn: function () {
return testOnAll(function (elem) {
return elem.active;
});
}
}
};
//utility function, Takes an array that has some property as its key
//and forms a javascript object with the keys as properties so we can get O(1) access
function createKeyMap(arr, propName) {
var keyMap = {}
for (var i = 0; i < arr.length; i++) {
keyMap[arr[i][propName]] = arr[i];
}
return keyMap;
}
Chart.types.PolarArea.extend({
name: "PolarAreaXF",
//this will have to be a member
dimension: undefined,
colorTypes: {
"NORMAL": 0,
"HIGHLIGHT": 1,
"FILTER": 2,
"FILTER_HIGHLIGHT": 3
},
chartGroup: undefined,
filters: undefined,
originalDataKeys: undefined,
initialize: function (data) {
//--PRE--
var that = this;
//Polar Area initialize method is expecting (data, options) in arguments,
//but we pass in an array of components to merge. Let's clean this up.
var argsArray = Array.prototype.slice.call(arguments);
//remove the first element of arguments which is our array, then we do a bunch of Chartjs converison on it . . .
argsArray.splice(0, 1);
//TODO - check if data is an array, if not, put a message in a console explaining how you are supposed to send data in an array
this.dimension = data.dimension;
data.chartGroup ? this.chartGroup = data.chartGroup : this.chartGroup = 0;
//short but magical line. Now we are linked with all dc charts in this group!
dc.registerChart(this, this.chartGroup);
var data = this.setupChartData(data.colors, data.highlights, data.labels);
//... and push the result in its place.
argsArray.unshift(data);
//originalDataArray -- this is used as a reference to the original state of the chart, since segments can come and go,
//we use this to track what a segment's original colors were when adding it back in. This would mess up adding a truly new segment, but who
//is gonna do that? Assumption here is dimensions start with so many groups and that is it.
this.originalDataKeys = createKeyMap(data, "key");
//parent's initialize
Chart.types.PolarArea.prototype.initialize.apply(this, argsArray);
//--modify SegmentArcs--
//assign colors and ids to all existing segment arcs
var mySegments = this.segments;
for (var i = 0; i < mySegments.length; i++) {
mySegments[i].colorList = [undefined, undefined, "#777", "#aaa"];
mySegments[i].colorList[this.colorTypes.NORMAL] = mySegments[i].fillColor;
mySegments[i].colorList[this.colorTypes.HIGHLIGHT] = mySegments[i].highlight;
mySegments[i].segmentID = i;
mySegments[i].key = data[i].key;
}
//add methods to SegmentArc objects that will color them one way or the other depending on their filter
this.SegmentArc.prototype.setIncluded = function (include) {
if (include) {
this.fillColor = this.colorList[that.colorTypes.NORMAL];
this.highlight = this.colorList[that.colorTypes.HIGHLIGHT];
} else {
this.fillColor = this.colorList[that.colorTypes.FILTER];
this.highlight = this.colorList[that.colorTypes.FILTER_HIGHLIGHT];
}
}
//--initialize filters--
this.filters = new FilterManager(this.segments);
//handle clicks on segments as filter events, do the styling and crossfilter changes at the Chart level in the filter method.
helpers.bindEvents(this, ["mousedown"], function (evt) {
var activeSegment = Chart.types.PolarArea.prototype.getSegmentsAtEvent.apply(this, [evt])[0];
this.handleFilter(activeSegment);
});
},
//convert crossfilter dimension into chart.js Polar Area data object array
setupChartData: function (colors, highlights, labels) {
var chartJSible = [];
//probably need checks here to make sure client actually passed in a crossfilter dimension
var grouped = this.dimension.group().reduceCount().top(Infinity);
//probably need checks here to either fail if the arrays aren't all long enough or have some way to add random colors/highlights if they are shorter.
for (var i = 0; i < grouped.length; i++) {
var dataObject = {
value: grouped[i].value,
key: grouped[i].key,
color: colors[i],
highlight: highlights[i],
label: labels ? (labels[i] ? labels[i] : grouped[i].key) : grouped[i].key
};
chartJSible.push(dataObject);
}
return chartJSible;
},
//figure out what changed between Chart.js' internally maintained data object array and crossfilter's dimension data. use the saved information
//about what colors and highlight a key has to rebuild the segmentArc list 'segments'. can't trash the old, it might mess up the animations.
redraw: function () {
var grouped = this.dimension.group().reduceCount().top(Infinity);
var currentSegmentKeys = createKeyMap(this.segments, "key");
var crossfilterGroupKeys = createKeyMap(grouped, "key");
//loop through the segment list, if the segment for a group is already there, update the value, if it is not there, add it back using the
//original data as a guide for what it's color and highlight color should be. if there are segments in the existing list
var length = Math.max(this.segments.length, grouped.length);
//going through both lists, whichever is longer
for (var i = 0; i < length; i++) {
var sList = this.segments;
var gList = grouped;
//only do this part if we still have items in the new filtered list
if (gList[i]) {
//we already have a segment for this crossfilter group, just get that segment and update its value
if (currentSegmentKeys[gList[i].key]) {
currentSegmentKeys[gList[i].key].value = gList[i].value;
} else {
//the chart doesn't have the crossfilter group item, add a new segment with the right colors and values from original data
var theSegment = this.originalDataKeys[gList[i].key];
this.addData(theSegment, 0, true);
}
}
//only do this part if we still have items in the current chart segment list
if (sList[i]) {
//we don't have this segment in the new crossfilter group, remove it from the chart
if (!crossfilterGroupKeys[sList[i].key]) {
this.removeData(i);
}
}
}
this.update();
},
filterAll: function () {
this.dimension.filterAll();
this.filters.turnAllOff();
this.colorMeIn();
this.redraw();
},
handleFilter: function (clicked) {
//after we have all of the filters figured out, change the colors to reflect what they should be and update the chart
this.filters.flip(clicked.segmentID);
this.colorMeIn();
if (this.filters.allOn()) {
this.dimension = this.dimension.filterAll();
dc.redrawAll(this.chartGroup);
this.filters.turnAllOff();
}
dc.redrawAll(this.chartGroup);
},
colorMeIn() {
var activeFilters = [];
var segments = this.segments;
for (var i = 0; i < segments.length; i++) {
var segment = segments[i];
if (this.filters.isActive(segment.segmentID) || this.filters.allOff()) {
segment.setIncluded(true);
activeFilters.push(segment.key);
} else {
segment.setIncluded(false);
}
}
this.dimension = this.dimension.filterFunction(function (d) {
for (var i = 0; i < activeFilters.length; i++) {
if (d === activeFilters[i]) {
return true;
}
}
return false;
});
}
})
}).call(this);
Use dc.js: https://dc-js.github.io/dc.js/
It has exactly the functionality asked for.
i had already done adding a click handler to each Segment of my doughnut chart with adding the following Code :
$("#myChart").click(
function(evt){
var activePoints = myNewChart.getSegmentsAtEvent(evt);
var chartelementid = activePoints[0].label;
alert(chartelementid);
//$('.details div').css("display", "none");
//$('#' + chartelementid).show();
}
);
This works fine, when finished it should display an additional Div with Details for this segment.
Unfortunality my labels are more then just Single Words, so i'm struggeling to create div ID's with the same name...
My Idea is to add to every Segment an additional Data like value,label, etc. so it could be an ID. but if i just add the ID information to the Segment it will not exist as variable.
Add DataType:
var dataImprove = [
{
value: 30,
color:"#001155",
highlight: "#1c2f7c",
label: "KnowHow Erhalt / Transfer & Aufbau",
id:"test"
}
]
where can i add in chart.js an additional dataType like shown above my ID to be accessible in the DOM?
kind regards Marco
As an alternative pass a JSON string as your label, then intercept to render. For example:
var canvas = document.getElementById(id);
var d = canvas.getContext("2d");
var chart = new Chart(d).Pie(json, {
segmentStrokeWidth: 1,
tooltipTemplate: "<%=label%>", //default the label
customTooltips: function (tooltip) {
// Hide if no tooltip
if (!tooltip) {
return;
}
var tooltipObj = JSON.parse(tooltip.text);
// etc
already found : between line 999 and 1023 in chart.js before drawing - i've added the line
id: ChartElements[0].id,
so the Data with the name ID is in the DOM avaiable.
I'm trying to implement a pull to reload/refresh type effect, that you see in lots of apps, with scrollview. I've got multiple pain points on this.
Just stopping a scrollview past where it wants to sit is a pain. Setting velocity or speed limit doesn't work, just setting the position just makes it bounce because it wants to continue back.
Then setting up an event call that doesn't just fire when they scroll too hard up while doing their normal browsing.
If anyone has thoughts on how to accomplish this I'd appreciate it. If I don't get any responses in a day I'll start posting my specific attempts and where they fell short. I can't believe I'm the only one trying to implement this pretty common feature.
You could use the FlexScrollView which supports this feature out of the box (it is nearly impossible to do it right with the stock famo.us scrollview..):
var FlexScrollView = require('famous-flex/FlexScrollView');
var RefreshLoader = require('famous-refresh-loader/RefreshLoader');
var scrollView = new FlexScrollView({
autoPipeEvents: true,
pullToRefreshHeader: new RefreshLoader({
size: [undefined, 60],
pullToRefresh: true,
color: 'green',
backgroundColor: 'white',
particleCount: 8,
particleSize: 7
})
});
Demo: https://rawgit.com/IjzerenHein/famous-flex-chat/master/dist/index.html
Repo: https://github.com/IjzerenHein/famous-flex
RefreshLoader: https://github.com/IjzerenHein/famous-refresh-loader
Tutorial: https://github.com/IjzerenHein/famous-flex/blob/master/tutorials/FlexScrollView.md
I finally have a solution for you. It relies only on start, and end events of the scrollview sync. To start you will see the scrollview.reset function. This function is taken for scrollviews internals to return _scroller to its default behavior.
We will use a transitionable and scrollview._scroller.positionFrom to control the behavior of the scrollview when it should not be default. Essentially everything is normalized on scrollview.sync.on 'start' and the update is carried out on scrollview.sync.on 'end' if the position of the scrollview has reached the refresh offset.
Much of the functionality is tied to the fact that I am using a timeout to simulate the load time of the refresh. It may be more appropriate to have a variable that is keeping track of whether a request is still being made.
Anyway, I hope this gives you some ideas.. Here is the full example.
var Engine = require('famous/core/Engine');
var Surface = require('famous/core/Surface');
var Transform = require('famous/core/Transform');
var Scrollview = require('famous/views/Scrollview');
var Transitionable = require('famous/transitions/Transitionable');
var SnapTransition = require('famous/transitions/SnapTransition');
Transitionable.registerMethod('snap',SnapTransition);
var snap = { method:'snap', period:200, dampingRatio:0.4 }
var context = Engine.createContext();
var scrollview = new Scrollview();
var surfaces = [];
scrollview.sequenceFrom(surfaces);
for (var i = 0; i < 20; i++) {
var surface = new Surface({
size: [undefined, 200],
properties: {
backgroundColor: "hsl(" + (i * 360 / 20) + ", 100%, 50%)",
}
});
surface.pipe(scrollview);
surfaces.push(surface);
}
scrollview.trans = new Transitionable(0);
// Timeout that will simulate loading time
scrollview.timer = null;
// Total simulated load time
scrollview.timeout = 500;
// Vertical offset scrollview will start load at
scrollview.refreshOffset = 100;
// Reset scroller to default behavior
scrollview.reset = function(){
scrollview._scroller.positionFrom(scrollview.getPosition.bind(scrollview));
}
scrollview.sync.on('start',function(){
clearTimeout(scrollview.timer);
scrollview.trans.halt();
var pos = scrollview.trans.get()
if (pos != 0) scrollview.setPosition(pos);
scrollview.reset()
});
scrollview.sync.on('end',function(){
var pos = scrollview.getPosition();
if (pos < (-scrollview.refreshOffset)) {
scrollview.trans.halt();
scrollview.trans.set(pos);
scrollview._scroller.positionFrom(function(){
return scrollview.trans.get();
});
scrollview.trans.set(-scrollview.refreshOffset,snap,function(){
scrollview.timer = setTimeout(function(){
scrollview.trans.halt();
scrollview.trans.set(0,snap,function(){
scrollview.reset()
});
}, scrollview.timeout );
});
} else {
scrollview.trans.halt();
scrollview.trans.set(0);
}
});
context.add(scrollview);