Using the Color Thief javascript effect, I have written some code that grabs an images dominant color and adjusts the entire theme's color scheme based on the image.
This all works well on a single product page, where only one image is being used. On my catalog page, there are multiple images that I need to grab the dominant color from. One image for each product. I need to display each separate color along with the product.
You can see the colored border in each product's panel (it's a brown/orange color).
My very slimmed-down code I'm using is below:
jQuery( document ).ready( function( $ ) {
var image = new Image;
var bg;
$('.post-image-hidden-container').each(function() {
bg = $(this).text();
image.onload = function() {
var colorThief = new ColorThief();
var dominantColor = colorThief.getColor(image);
var colorPalette = colorThief.getPalette(image, 7);
var backgroundColor = 'rgb('+ dominantColor +')';
/* Calculate the Lightest Color in the Palette */
var lightestColor = colorPalette.reduce(function(previousValue, currentValue) {
var currLightNess = (0.2126*currentValue[0] + 0.7152*currentValue[1] + 0.0722*currentValue[2]);
var prevLightNess = (0.2126*previousValue[0] + 0.7152*previousValue[1] + 0.0722*previousValue[2]);
return (prevLightNess < currLightNess) ? currentValue : previousValue;
});
/* Calculate the Darkest Color in the Palette */
var darkestColor = colorPalette.reduce(function(previousValue, currentValue) {
var currLightNess = (0.2126*currentValue[0] + 0.7152*currentValue[1] + 0.0722*currentValue[2]);
var prevLightNess = (0.2126*previousValue[0] + 0.7152*previousValue[1] + 0.0722*previousValue[2]);
return (prevLightNess > currLightNess) ? currentValue : previousValue;
});
/* Create Shades and Tints of Lightest Color */
...
/* Shades (darker) */
...
/* Tints (lighter) */
...
/* Handle all CSS based on returned colors */
$('.product-bottom-info-container').each(function() {
$(this).css({
borderTop: '3px solid rgb('+ lightestColor +')'
});
});
}
image.src = bg;
});
});
Right after declaring the variable bg I wrapped the entire thing in an each() loop. Down towards the bottom, .product-bottom-info-container is the element where the colored border appears. I cannot seem to get each product's border to be it's own color. It keeps giving every border the LAST color in the loop.
Some notes:
.post-image-hidden-container is a hidden div above each product that contains the product's image url.
.product-bottom-info-container is the container at the bottom of each product with the product title and the colored border.
Am I using the each() function properly? What am I doing wrong?
Thanks
UPDATE
I was able to grab all of the RGB values for each image and put them into an array:
var thisColor;
var theseColors = [];
$('.shop-page-item-thumb').each(function() {
$(this).find('img').each(function() {
thisColor = colorThief.getColor(this);
theseColors.push(thisColor);
});
});
Now that I have all RGB's available, is there a way to simply loop through this array and assign each value to its respective .product-bottom-info-container element?
theseColors[0] is the first RGB, theseColors[1] is the second, etc. all the way up to theseColors[11].
If I run console.log(thisColor) inside of the loop, I get these results:
[29, 28, 22]
[217, 195, 189]
[14, 14, 8]
[233, 232, 206]
[31, 31, 31]
[82, 97, 111]
[60, 68, 84]
[34, 29, 30]
[17, 30, 37]
[12, 11, 12]
[56, 43, 26]
[209, 150, 108]
The 12 RGB values that I need. So we're headed in the right direction.
UPDATE for Mottie
Here is the HTML structure for one of the products. .shop-page-item-thumb is the container that holds the thumbnail, but .shop-page-item-article is the parent (besides the actual li list item).
FINAL UPDATE (Thanks Mottie!)
This is the piece of code that finally worked:
$('.shop-page-item-thumb').each(function() {
var thumb = $(this);
thumb.find('img').each(function() {
thisColor = colorThief.getColor(this);
thumb.parent().find('.product-bottom-info-container').css({
borderTop: '3px solid rgb('+ thisColor +')'
})
});
});
Much love, Stack Overflow! <3
It looks like this code is looping through each container after each image load...
/* Handle all CSS based on returned colors */
$('.product-bottom-info-container').each(function() {
$(this).css({
borderTop: '3px solid rgb('+ lightestColor +')'
});
});
try this instead:
$(this).closest('.product-bottom-info-container').css({
borderTop: '3px solid rgb('+ lightestColor +')'
});
Update: Oh sorry, I didn't look at the code that closely... another problem is the image definition. There is only one, not one for each container... Instead of defining it outside the loop, find it inside of the .each loop and then attach an onload...
$(this).find('img')[0].onload = function() {
It shouldn't change the above code that adds the border color since this would refer to the image inside of the onload function.
It would have been easier to troubleshoot the problem if you would have provided a ready-made demo to use.
Update2: Instead of pushing the colors to an array, apply them directly to the border; I have no idea about the thumb images in relation to the containers, so we're just going to assume that the thumbs are in the same order as the containers. Some HTML would have helped. Updated for the given HTML...
$('.shop-page-item-thumb').each(function() {
var thumb = $(this);
thumb.find('img').each(function() {
thisColor = colorThief.getColor(this);
// prev() targets the previous element (it should be
// the 'product-bottom-info-container'; or use
// thumb.parent().find('.product-bottom-info-container')
thumb.prev().css({
borderTop: '3px solid rgb('+ thisColor +')'
});
});
});
I'm building a small UI which provides users progress of downloading or loading certain information. Here is the codes so far
http://jsfiddle.net/pge1wukj/4/
var s = Snap("svg");
var movepath = s.select("#movePath").attr({
"fill":"none"
});
var dapath = s.select("#dapath").attr({
stroke: "#cdcdcd",
"stroke-width": 5,
});
var dapoints = [242,334.5, 372,334.5, 372,390, 320.5,390.5 ,308.5,421.5 ,293.5,391.5 ,242,391]
var circle = s.select("circle");
var poly = s.select("polygon");
$("a").click(function(){
circle.animate({
opacity: 0
},100);
poly.polyAnimate(dapoints,100,mina.linear,function(){
moveAlongPath(poly,{x:308,y:421},s.halfArc(308,421,135,382,50,0),100);
});
dapath.animate({
d:"M135,382.5c0,52.159,85,79.031,170.001,79.498 C 390.3,462.466,475.598,436.344,475,382.5",
},100,function(){
dapath.animate({d:"M135,382.5c0,0,85.999-0.467,171,0c85.299,0.468,169,0,169,0"
},100,function(){
dapath.animate({
d:"M135,382.292c0,0,85.999-22.759,171-22.292c85.299,0.468,169,22.292,169,22.292"
},100,function(){
dapath.animate({
d:"M136,382.415c0,0,90.999,13.118,176,13.585c85.299,0.468,164-13.585,164-13.585"
},100,function(){
dapath.animate({
d:"M135,382.5c0,0,85.999-0.467,171,0c85.299,0.468,169,0,169,0"
},500,mina.bounce,function(){
var pathclone = dapath.clone().attr({
stroke: "blue",
strokeDashoffset: 500,
strokeDasharray: 500
});
var datext = s.text(100,330,"90").attr("style","text-align: center");
var banner = s.group(poly,datext);
moveAlongPath(banner,{x:136,y:382.415},movepath,3600);
var tick = 0;
var interval = setInterval(function(){
tick += 1;
var red = Math.random()*255;
var blue = Math.random()*255;
var green = Math.random()*255;
var hex = Snap.rgb(red,green,blue);
var dadatext = $("text").text(tick+" %")
if(tick % 10 == 0){
dadatext.attr({"font-size":"30px","fill":hex});
};
if (tick >= 100){clearTimeout(interval)};
},36);
pathclone.animate({
"stroke-dashoffset":0,
},5300);
var paths = Snap.set().push(dapath,pathclone);
paths.animate({
d:"M135,382.5c0,0,30,17,42,17c10,0.208,298-17,298-17"
},300,function(){
paths.animate({
d:"M135,382.5c0,0,287,17.208,297,17c12,0,43-17,43-17"
},2900,function(){
paths.animate({d:"M135,382.5c0,0,287,17.208,297,17c12,0,43-17,43-17"},100,function(){
paths.animate({
d:"M135,382.5c0,0,85.999-0.467,171,0c85.299,0.468,169,0,169,0"
},200,function(){
$("a").off("click");
/* End of animation */
/* Ready for next transformation*/
banner.animate({
transform: "rotate(180deg)"
},200);
})
})
});
})
});
});
})
})
});
});`
At the end of the animation, the banner should rotate 180 deg on the sharp point. However it doesn't animate as i expected. Is there any solution to this ? transforming animation is intimidating and i don't fully understand it....
I think what you are missing is that you need to provide the original transform to include, otherwise it will assume you are just replacing that transform.
So with this line...
banner.animate({ transform: 'rotate(180)' },200);
What this really means is, I'm going to overwrite any current transforms and animate to a new one of rotate(180).
What you probably want is...keep any existing transforms, and now rotate(180) as well.
So you probably want something more like this...transform() with no parameters will give us the existing transform. So we can combine.
transform -> existingtransform then apply additional transform
This would look like the following.
banner.animate({ transform: banner.transform() + " s-1,1" },200);
I'm not quite sure of the rotation effect you are after (did you mean it to go upside down or back to front?), but 's-1,1' may have been what you were thinking of.
jsfiddle example
Edit: jsfiddle with alternate rotation.
Note, for this, you need to take into account 'where' the polygon is, in relation to the group, as its offset as you have moved it (and then also moved the group).
Edit: The rotation center point is quite difficult, as we have the polygon points offset (not centred around 0) and then translated. Also the group they are in is translated, so you've kinda of got 3 things going on.
To try and help understand getting the actual points, I have rewritten the rotation animation.
banner.animate({ transform: banner.transform() + "r180," + banner.getBBox(1).cx + ',' + banner.getBBox(1).y2 },200);
We get the bounding box, which calculates the centre for us. The pivot point is mid length cx, and the lowest y point is y2.
I suspect there is an easier way to get the whole thing working to reduce the transform complexity in the overall code, but there's a bit too much to break down for a question here.
jsfiddle with getBBox
I have a multi-bar chart in which I've assigned a click event to the bars. This works fine until a user changes the chart type from grouped to stacked, at which point I've discovered that I need to reassign the onClick handler. This all seems to work correctly.
The problem is that after my click handler runs, whether or not the user has changed the chart type yet previously, attempting to change the chart type will result in a "groups.exit(...).watchTransition is not a function" JS error.
Chart definition:
nv.addGraph(function() {
// Defining the chart itself
var chart = nv.models.multiBarHorizontalChart()
.x(function(d) { return d.label })
.y(function(d) { return d.value })
.margin({top: 30, right: 20, bottom: 50, left: 275})
.showValues(true) //Show bar value next to each bar.
.tooltips(true) //Show tooltips on hover.
.valueFormat(d3.format('$,.2f'))
.groupSpacing(0.5)
.showControls(true); //Allow user to switch between "Grouped" and "Stacked" mode.
chart.yAxis
.tickFormat(d3.format('$,.2f'));
d3.select('#chart2 svg')
.datum(barData)
.call(chart);
nv.utils.windowResize(chart.update);
return chart;
},
function(){
// Set the click handler. This part works fine, but the onclick handler goes away after changing the chart type and thus needs redefined below.
// PROBLEM POINT: once this code is run, the user can no longer change the chart type. They just keep getting "groups.exit(...).watchTransition is not a function"
d3.selectAll(".nv-bar").on('click',
function(e){
var canName = e.label.split('(');
var canName = $.trim(canName[0]);
var searchTerm = canName + ' ' + e.key;
var detUrl = "/details.cfm?canName=" + encodeURIComponent(canName) + "&searchTerm=" + encodeURIComponent(searchTerm);
$("#detailsDiv").html("Loading...");
$("#detailsDiv").load(detUrl);
location.href = "#details";
});
// If I try to redefine the bar click handler in the radio button's "click" event it overwrites the built in JS used to change the chart type, so instead
// I handle it onMouseUp.
d3.selectAll(".nv-series").on('mouseup',
function(e){
setTimeout(function(){
// Just running this directly on mouseUp doesn't work. Apparently the chart needs time to load first. So we do it 100ms later, which works fine.
d3.selectAll(".nv-bar").on('click',
function(e){
var canName = e.label.split('(');
var canName = $.trim(canName[0]);
var searchTerm = canName + ' ' + e.key;
var detUrl = "/details.cfm?canName=" + encodeURIComponent(canName) + "&searchTerm=" + encodeURIComponent(searchTerm);
$("#detailsDiv").html("Loading...");
$("#detailsDiv").load(detUrl);
location.href = "#details";
});
}, 100);
});
});
watchTransition is defined by nvd3 on D3's selection prototype, if you have nv.d3.js loaded in the browser, you should be able to step with the debugger into the following code before, any chart is rendered:
d3.selection.prototype.watchTransition = function(renderWatch){
var args = [this].concat([].slice.call(arguments, 1));
return renderWatch.transition.apply(renderWatch, args);
};
I had the same issue. The reason was that I was using webpack, which bundled D3 inside my application, so the D3 that was used to draw the chart was not the D3 that NVD3 visits to add the function to the prototype. So if you are using webpack or browserify make sure to exclude D3 and add it only as reference script.
We solved this issue by downgrading to d3 3.4.4, as advised by this comment.
We're phasing out the Highcharts javascript visualization lib from our interactive statistics research application. It was already replaced with Rickshaw. Just now a new request came in: One certain use case has the graph display with the measurements displayed in the graph directly. This has been the case while using Highcharts (which has an option for that; called dataLabelsActivated). That should still be the case when using Rickshaw. I haven't yet found an option to make it do that. Any ideas?
How it used to display with Highcharts - highlighted in red are the measurements that should be there when using Rickshaw:
How it currently display with Rickshaw:
Apparently rickshaw doesn't support this natively. I might've done that by extending rickshar through the d3 library it is based upon (which seems to be able to do what I intended to achieve, according to the examples on its website). However, I ended up with a simple solution - added the data labels as divs manually, dependent on the distance of each datapoint from the top left corner of the graph element. Below code searches the data attribute of the graph for the data to display in labels using the color of the datapoint as it is the sole item to match a datapoint with the information in the data attribute.
$(".pointMarker").each(function( index ) {
var percentage = 0;
var currentMarkerColor = self.rgb2hex($( this ).css("border-top-color"));
self.graph.series.forEach(function(series) {
if(currentMarkerColor === series.color) {
if ( !/undef/i.test(typeof series.data[index])) {
percentage = parseFloat(series.data[index].y).toFixed(2);
}
//end loop
return false;
}
});
if (percentage > 0) {
var totalHeight = $( this ).parent().height();
var distanceTop = $( this ).css("top").replace(/[^-\d\.]/g, '') ;
//display data
$( this ).parent().append( "<div class='dataLabel' style='top:"+(parseInt($(this).css('top'), 10)-5)+"px;left:"+(parseInt($(this).css('left'), 10)-9)+"px;height:100px;width:100px;'>"+percentage+"</div>" );
}
});
and
this.rgb2hex = function (rgb){
rgb = rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/);
return "#" +
("0" + parseInt(rgb[1],10).toString(16)).slice(-2) +
("0" + parseInt(rgb[2],10).toString(16)).slice(-2) +
("0" + parseInt(rgb[3],10).toString(16)).slice(-2);
}
I was having a hard time trying to figure out how to center labels on a datetime x-axis in Highcharts without using categories and tickPlacement (since tickPlacement only works on categories).
My axis was dynamically created so I could not simply set an x-offset or padding, as this would cause axes of different intervals to look strange.
After messing around with the config options I think I may have found a solution using the x-axis formatter and some css / jquery noodling in the Highcharts callback. See my answer below.
The trick is to use the x-axis labels object like this:
xAxis: {
type: 'datetime',
labels: {
useHTML: true,
align: 'center',
formatter: function () {
//using a specific class for the labels helps to ensure no other labels are moved
return '<span class="timeline_label">' + Highcharts.dateFormat(this.dateTimeLabelFormat, this.value) + '</span>';
}
}
You can see that the formatter will keep whatever dateTimeLabelFormat has been set by the user or default.
Then have a callback that does something like this:
function (chart) {
var $container = $(chart.container);
var $labels = $container.find('.highcharts-axis-labels .timeline_label');
var $thisLabel, $nextLabel, thisXPos, nextXPos, delta, newXPos;
$labels.each(function () {
$thisLabel = $(this).parent('span');
thisXPos = parseInt($thisLabel.css('left'));
$nextLabel = $thisLabel.next();
nextXPos = $nextLabel.length ? parseInt($nextLabel.css('left')) : chart.axes[0].left + chart.axes[0].width;
delta = (nextXPos - thisXPos) / 2.0;
newXPos = thisXPos + delta;
if ($nextLabel.length || $(this).width() + newXPos < nextXPos) {
$thisLabel.css('left', newXPos + 'px');
} else {
$thisLabel.remove();
}
});
});
In short, this will go through each label and determine how much it should be moved over (using css) by calculating the distance between itself and the next label. When it reaches the the last label, it either moves it over using the end of the axis for the calculation or removes it if it won't fit. This last part is just the decision I decided to make, you can probably choose to do something else like word wrap, etc.
You can see the jsfiddle here
Hope this helps some people. Also, if there are any improvements it would be great to see them here.
Based on the existing answer, there is a much simpler solution that also works when resizing the browser window (or otherwise forcing the chart to redraw), even when the tick count changes: http://jsfiddle.net/McNetic/eyyom2qg/3/
It works by attaching the same event handler to both the load and the redraw events:
$('#container').highcharts({
chart: {
events: {
load: fixLabels,
redraw: fixLabels
}
},
[...]
The handler itself looks like this:
var fixLabels = function() {
var labels = $('div.highcharts-xaxis-labels span', this.container).sort(function(a, b) {
return +parseInt($(a).css('left')) - +parseInt($(b).css('left'));
});
labels.css('margin-left',
(parseInt($(labels.get(1)).css('left')) - parseInt($(labels.get(0)).css('left'))) / 2
);
$(labels.get(this.xAxis[0].tickPositions.length - 1)).remove();
};
As you see, the extra wrapping of labels is unnecessary (at least if you do not have more than one xAxis). Basically, it works like this:
Get all existing labels (when redrawn, this includes newly added ones). 2. Sort by css property 'left' (they are not sorted this way after some redrawing)
Calculate offset between the first two labels (the offset is the same for all labels)
Set half of the offset as margin-left of all labels, effectively shifting them half the offset to the right.
Remove the rightmost label (moved outside of chart, by sometimes partly visible).