I have a custom clipping, not default one. In my case I want to see image that out of clipping area and watermark. All works fine, but I can't change background color of empty area behind my image.
Here's working fiddle.
I've tried to set c.backgroundColor = 'red' or c.overlayColor = 'red', but it doesnt work.
EDIT: example of solution could be found in this fiddle.
ItÂ's not working because, you have set the globalCompositeOperation property to destination-atop for the image object, which changes the drawing destination of the background color. There is no need to set this property for the image object at all.
Here is the working version of your code ...
var c = new fabric.Canvas('c');
c.setBackgroundColor('white'); //set canvas background color
var clip = {
left: 100,
top: 100,
right: c.getWidth() - 100,
bottom: c.getHeight() - 100
}
var rb = new fabric.Path('M 0 0 H ' + c.getWidth() + ' V ' + clip.top + ' H ' + clip.left + ' V ' + clip.bottom + ' H ' + clip.right + ' V ' + clip.top + ' H ' + c.getWidth() + ' V ' + c.getHeight() + ' H 0 Z', {
left: 0,
top: 0,
fill: 'rgba(100,50,31,0.3)'
});
var circle = new fabric.Circle({
left: clip.right - 40,
top: clip.bottom - 40,
radius: 40,
fill: 'bisque'
});
var g = new fabric.Group([rb, circle], {
left: 0,
top: 0
});
c.setOverlayImage(g);
fabric.Image.fromURL('data:image/png;base64,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', function(fimg) {
c.add(fimg.set({
left: clip.left - 50,
top: clip.top - 50,
//globalCompositeOperation: 'destination-atop' //<-- do not set this
}));
c.setActiveObject(c.item(0));
})
c.renderAll();
fabric.Canvas.prototype.cropImage = function(format, quality, x, y, width, height) {
var canvasEl = this.lowerCanvasEl || this.upperCanvasEl;
this.deactivateAll().renderAll(true);
var printEl = canvasEl;
// Only use extra canvas if any optional param is given
if (x || y || width || height) {
// Defaults
x = x || 0;
y = y || 0;
width = width || canvasEl.width - x;
height = height || canvasEl.height - y;
// create printCanvas if necessary
if (!fabric.printCanvas)
fabric.printCanvas = document.createElement('canvas');
printEl = fabric.printCanvas;
// Calculate spaces over canvas bounds
var d = {
left: (x > 0) ? 0 : -x,
top: (y > 0) ? 0 : -y,
right: (x + width < canvasEl.width) ? 0 : (width + x) - canvasEl.width,
bottom: (y + height < canvasEl.height) ? 0 : (height + y) - canvasEl.height
};
// Print section
printEl.width = width;
printEl.height = height;
printEl.getContext('2d').drawImage(canvasEl,
x + d.left, y + d.top, width - d.right - d.left, height - d.bottom - d.top,
d.left, d.top, width - d.right - d.left, height - d.bottom - d.top);
}
var data = (fabric.StaticCanvas.supports('toDataURLWithQuality')) ? printEl.toDataURL('image/' + format, quality) : printEl.toDataURL('image/' + format);
this.contextTop && this.clearContext(this.contextTop);
this.renderAll();
return data;
};
$('#clip').click(function() {
window.open(c.cropImage('png', 75, clip.left, clip.top, clip.right - clip.left, clip.bottom - clip.top));
});
canvas{outline: 2px solid black;}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.7.13/fabric.min.js"></script>
<canvas id="c" width="400" height="400"></canvas>
<button id="clip">
Clip
</button>
and a JSFiddle
Related
I have a plot where there are some fixed theoretical points and the experimental points are plotted using a combination of multi-select buttons. Is there a way I can generate data of the %error between the theoretical points and the experimental data?
An example of the plot is shown below:
This code displays the percent error as text next to the theoretical point. You can also draw a line on the canvas from theoretical to experimental point and the difference data will be displayed in a tooltip. The first click (button release) initiates the "difference" line. The next click ends it. The tooltip remains always on the plot but you could easily tweak it so it disappears after the second click.
import numpy as np
from bokeh.plotting import figure, show
from bokeh.models import ColumnDataSource, CustomJS
from bokeh.events import *
source = ColumnDataSource({'x': [], 'y': []})
p = figure(plot_width = 900)
x_data = np.arange(10)
source_expected = dict(x = x_data, y = np.random.random(10), color=['blue']*10)
source_measured = dict(x = x_data, y = np.random.random(10), color=['grey']*10)
line_expected = p.line('x', 'y', line_color = 'blue', source = source_expected)
circle_expected = p.circle('x', 'y', color = 'color', source = source_expected, size = 10)
line_measured = p.line('x', 'y', line_color = 'grey', source = source_measured)
# circle_measured = p.circle('x', 'y', color = 'color', fill_color = 'white', source = source_measured, size = 60)
circle_measured = p.circle('x', 'y', color = 'color', source = source_measured, size = 10)
source_error = {'x': [], 'y': [], 'error': [], 'percent': [], 'text': [], 'color': []}
i = 0
for expected, measured in zip(source_expected['y'], source_measured['y']):
source_error['x'].append(x_data[i])
source_error['y'].append(measured)
error = expected - measured
percent = (error/expected) * 100
source_error['error'].append(error)
source_error['percent'].append(percent)
source_error['text'].append('+{}%'.format('%.2f' % percent) if percent > 0 else '{}%'.format('%.2f' % percent))
source_error['color'].append('green' if expected == measured else 'red')
i = i + 1
# text_measured = p.text('x', 'y', text = 'text', text_color = 'color', text_font_size = '9pt', x_offset = -25, y_offset = 6, source = source_error)
text_measured = p.text('x', 'y', text = 'text', text_color = 'color', text_font_size = '9pt', x_offset = 10, y_offset = 5, source = source_error)
line = p.line('x', 'y', line_color = 'red', line_dash = 'dashed', source = source)
callback_tap = '''
if (typeof custom_tooltip == 'undefined') {
custom_tooltip = document.createElement('div');
custom_tooltip.setAttribute('id','tooltip_div');
custom_tooltip.style = 'position: absolute; left: 0px; top: 0px; z-index: 9999; border:1px solid black; padding: 10px; background: white; font-family: arial; font-size: 12px'
document.body.prepend(custom_tooltip);
}
if (true === Bokeh.drawing) {
Bokeh.drawing = false
}
else {
if (!Bokeh.drawing) {
src.data = {'x':[], 'y':[]}
src.change.emit()
}
src.data['x'].push(cb_obj.x)
src.data['y'].push(cb_obj.y)
Bokeh.drawing = true
Bokeh.sx_start = cb_obj.sx
Bokeh.x_start = cb_obj.x
Bokeh.sy_start = cb_obj.sy
Bokeh.y_start = cb_obj.y
}'''
callback_mousemove = '''
function print(...args) {
for (i in args) {
console.log(args[i])
}
}
if (Bokeh.drawing) {
if (src.data['x'].length > 1) {
src.data['x'].pop()
src.data['y'].pop()
}
src.data['x'].push(cb_obj.x)
src.data['y'].push(cb_obj.y)
src.change.emit()
tooltip = document.getElementById('tooltip_div')
tooltip.style.left = cb_obj.sx + 30 + 'px'
tooltip.style.top = cb_obj.sy + 10 + 'px'
var error = Math.round((Bokeh.y_start - cb_obj.y) * 100) / 100
var percent = Math.round(error * 100 / Bokeh.y_start)
tooltip.innerHTML = 'Distance X: ' + Math.round(Bokeh.sx_start - cb_obj.sx) + ' px' + ' (' + (Math.round((Bokeh.x_start - cb_obj.x) * 100) / 100) + ' units)' +
'<br />' +
'Distance Y: ' + Math.round(Bokeh.sy_start - cb_obj.sy) + ' px' + ' (' + (Math.round((Bokeh.y_start - cb_obj.y) * 100) / 100) + ' units)' +
'<br />' +
'Error: ' + error + ' units' +
'<br />' +
'% Error: ' + percent + ' %'
}'''
p.js_on_event('tap', CustomJS(args = {'src': source, }, code = callback_tap))
p.js_on_event('mousemove', CustomJS(args = {'src': source, }, code = callback_mousemove))
show(p)
Result:
Swapping the commented lines with the lines next to them makes the error percent to be displayed in a big circle representing the theoretical point. It looks then like this:
This script was written for Python v3.7.2 and Bokeh v1.3.0
I'm trying to create an funnel chart like this with svg. http://i.stack.imgur.com/hUyru.jpg
My first attempt was with svg filter effects, but then I found out that svg filter effects aren't supported in IE.
The second attempt was with svg paths but I can't manage to transform the path based on the previous circle.
http://codepen.io/justpixel/pen/MwOLRQ
<path transform="translate(0 27) scale(0.9 0.6)" fill="#ED1C24" d="M240.208,110.922c-43.5-29-140.417,19.125-175.322,19.125V0c34.906,0,131.822,50.422,175.333,18.667
L240.208,110.922z"/>
Do you have any tips on how can I do this?
It's quite simple. as long as you know how to create SVG elements with JS - and you know a little bit of trigonometry.
var svgns = "http://www.w3.org/2000/svg";
// Make the graph
var radius = [88, 66, 56, 27];
var inter_circle_gap = 80;
var startX = 120;
var startY = 120;
var funnelSqueezeFactor = 0.3;
// Draw the funnels
var g = document.getElementById("funnels");
var x = startX; // centre of first circle
var numFunnels = radius.length - 1;
for (var i=0; i<numFunnels; i++)
{
nextX = x + radius[i] + inter_circle_gap + radius[i+1];
makeFunnel(g, x, nextX, startY, radius[i], radius[i+1]);
x = nextX;
}
// Draw the circles
var g = document.getElementById("circles");
var x = startX - radius[0]; // left edge of first circle
for (var i=0; i<radius.length; i++)
{
x += radius[i]; // centre X for this circle
makeCircle(g, x, startY, radius[i]);
x += radius[i] + inter_circle_gap; // step to left edge of next circle
}
// Function to make a circle
function makeCircle(g, x, y, r)
{
var circle = document.createElementNS(svgns, "circle");
circle.setAttribute("cx", x);
circle.setAttribute("cy", y);
circle.setAttribute("r", r);
g.appendChild(circle);
}
// Function to make a funnel
function makeFunnel(g, x1, x2, y, r1, r2)
{
var tangentAngle = 30 * Math.PI / 180;; // 30 degrees
startPointX = r1 * Math.sin(tangentAngle);
startPointY = r1 * Math.cos(tangentAngle);
endPointX = r2 * Math.sin(tangentAngle);
endPointY = r2 * Math.cos(tangentAngle);
ctrlPointX = (x1 + x2) / 2;
ctrlPointY = (startPointY + endPointY) * funnelSqueezeFactor / 2;
var d = 'M' + (x1 + startPointX) + ',' + (y - startPointY);
d += ' Q' + ctrlPointX + ',' + (y - ctrlPointY) + ','
+ (x2 - endPointX) + ',' + (y - endPointY);
d += ' L' + (x2 - endPointX) + ',' + (y + endPointY);
d += ' Q' + ctrlPointX + ',' + (y + ctrlPointY) + ','
+ (x1 + startPointX) + ',' + (y + startPointY);
d += "Z";
var path = document.createElementNS(svgns, "path");
path.setAttribute("d", d);
g.appendChild(path);
}
#circles circle {
fill: #27293d;
}
#funnels path {
fill: #f5d135;
}
<svg width="779px" height="306px">
<g id="funnels"></g>
<g id="circles"></g>
</svg>
I have created a small Raphael app to showcase my struggle.
I created four handles which can be moved. A 'sheet' is covering the entire screen except for the square between the 4 handles.
Whenever the handles are dragged the sheet is placed accordingly.
What ends up happening is that in certain situations, the sheet folds on itself.
It's best if you just see the fiddle. You'll get what I'm talking about
http://jsfiddle.net/8qtffq0s/
How can I avoid this?
Notice: The screen is white. The black part is the sheet, and the white part is a gap in the sheet and not the other way around.
//raphael object
var paper = Raphael(0, 0, 600, 600)
//create 4 handles
h1 = paper.circle(50, 50, 10).attr("fill","green")
h2 = paper.circle(300, 50, 10).attr("fill", "blue")
h3 = paper.circle(300, 300, 10).attr("fill", "yellow")
h4 = paper.circle(50, 300, 10).attr("fill", "red")
//create covering sheet
path = ["M", 0, 0, "L", 600, 0, 600, 600, 0, 600, 'z', "M", h1.attrs.cx, h1.attrs.cy,"L", h4.attrs.cx, h4.attrs.cy, h3.attrs.cx, h3.attrs.cy, h2.attrs.cx, h2.attrs.cy,'z']
sheet = paper.path(path).attr({ "fill": "black", "stroke": "white" }).toBack()
//keep starting position of each handle on dragStart
var startX,startY
function getPos(handle) {
startX= handle.attrs.cx
startY = handle.attrs.cy
}
//Redraw the sheet to match the new handle placing
function reDrawSheet() {
path = ["M", 0, 0, "L", 600, 0, 600, 600, 0, 600, 'z', "M", h1.attrs.cx, h1.attrs.cy, "L", h4.attrs.cx, h4.attrs.cy, h3.attrs.cx, h3.attrs.cy, h2.attrs.cx, h2.attrs.cy, 'z']
sheet.attr("path",path)
}
//enable handle dragging
h1.drag(function (dx, dy) {
this.attr("cx", startX + dx)
this.attr("cy", startY + dy)
reDrawSheet()
},
function () {
getPos(this)
})
h2.drag(function (dx, dy) {
this.attr("cx", startX + dx)
this.attr("cy", startY + dy)
reDrawSheet()
},
function () {
getPos(this)
})
h3.drag(function (dx, dy) {
this.attr("cx", startX + dx)
this.attr("cy", startY + dy)
reDrawSheet()
},
function () {
getPos(this)
})
h4.drag(function (dx, dy) {
this.attr("cx", startX + dx)
this.attr("cy", startY + dy)
reDrawSheet()
},
function () {
getPos(this)
})
Update: I improved the function "reDrawSheet" so now it can classify the points on the strings as top left, bottom left, bottom right, and top right
This solved many of my problems, but in some cases the sheet still folds on it self.
new fiddle: http://jsfiddle.net/1kj06co4/
new code:
function reDrawSheet() {
//c stands for coordinates
c = [{ x: h1.attrs.cx, y: h1.attrs.cy }, { x: h4.attrs.cx, y: h4.attrs.cy }, { x: h3.attrs.cx, y: h3.attrs.cy }, { x: h2.attrs.cx, y: h2.attrs.cy }]
//arrange the 4 points by height
c.sort(function (a, b) {
return a.y - b.y
})
//keep top 2 points
cTop = [c[0], c[1]]
//arrange them from left to right
cTop.sort(function (a, b) {
return a.x - b.x
})
//keep bottom 2 points
cBottom = [c[2], c[3]]
//arrange them from left to right
cBottom.sort(function (a, b) {
return a.x - b.x
})
//top left most point
tl = cTop[0]
//bottom left most point
bl = cBottom[0]
//top right most point
tr = cTop[1]
//bottom right most point
br = cBottom[1]
path = ["M", 0, 0, "L", 600, 0, 600, 600, 0, 600, 'z', "M", tl.x,tl.y, "L", bl.x,bl.y, br.x,br.y, tr.x,tr.y, 'z']
sheet.attr("path",path)
}
To make things super clear, this is what I'm trying to avoid:
Update 2:
I was able to avoid the vertices from crossing by checking which path out of the three possible paths is the shortest and choosing it.
To do so, I added a function that checks the distance between two points
function distance(a, b) {
return Math.sqrt(Math.pow(b.x - a.x, 2) + (Math.pow(b.y - a.y, 2)))
}
And altered the code like so:
function reDrawSheet() {
//c stands for coordinates
c = [{ x: h1.attrs.cx, y: h1.attrs.cy }, { x: h4.attrs.cx, y: h4.attrs.cy }, { x: h3.attrs.cx, y: h3.attrs.cy }, { x: h2.attrs.cx, y: h2.attrs.cy }]
//d stands for distance
d=distance
//get the distance of all possible paths
d1 = d(c[0], c[1]) + d(c[1], c[2]) + d(c[2], c[3]) + d(c[3], c[0])
d2 = d(c[0], c[2]) + d(c[2], c[3]) + d(c[3], c[1]) + d(c[1], c[0])
d3 = d(c[0], c[2]) + d(c[2], c[1]) + d(c[1], c[3]) + d(c[3], c[0])
//choose the shortest distance
if (d1 <= Math.min(d2, d3)) {
tl = c[0]
bl = c[1]
br = c[2]
tr = c[3]
}
else if (d2 <= Math.min(d1, d3)) {
tl = c[0]
bl = c[2]
br = c[3]
tr = c[1]
}
else if (d3 <= Math.min(d1, d2)) {
tl = c[0]
bl = c[2]
br = c[1]
tr = c[3]
}
path = ["M", 0, 0, "L", 600, 0, 600, 600, 0, 600, 'z', "M", tl.x,tl.y, "L", bl.x,bl.y, br.x,br.y, tr.x,tr.y, 'z']
sheet.attr("path",path)
}
Now the line does not cross itself like the image I attached about, but the sheet "flips" so everything turns black.
You can see the path is drawn correctly to connect the for points by the white stroke, but it does not leave a gap
new fiddle: http://jsfiddle.net/1kj06co4/1/
Picture of problem:
So... the trouble is to tell the inside from the outside.
You need the following functions:
function sub(a, b) {
return { x: a.x - b.x , y: a.y - b.y };
}
function neg(a) {
return { x: -a.x , y: -a.y };
}
function cross_prod(a, b) {
// 2D vecs, so z==0.
// Therefore, x and y components are 0.
// Return the only important result, z.
return (a.x*b.y - a.y*b.x);
}
And then you need to do the following once you've found tl,tr,br, and bl:
tlr = sub(tr,tl);
tbl = sub(bl,tl);
brl = sub(bl,br);
btr = sub(tr,br);
cropTL = cross_prod( tbl, tlr );
cropTR = cross_prod(neg(tlr),neg(btr));
cropBR = cross_prod( btr, brl );
cropBL = cross_prod(neg(brl),neg(tbl));
cwTL = cropTL > 0;
cwTR = cropTR > 0;
cwBR = cropBR > 0;
cwBL = cropBL > 0;
if (cwTL) {
tmp = tr;
tr = bl;
bl = tmp;
}
if (cwTR == cwBR && cwBR == cwBL && cwTR!= cwTL) {
tmp = tr;
tr = bl;
bl = tmp;
}
My version of the fiddle is here. :) http://jsfiddle.net/1kj06co4/39/
I am creating two elements (1. arrow shape and 2. dotted line) using path (Raphael and SVG) and I want to drag these two together but I am only able to drag it independently. Here is my code for this:
gaugeSvg = Raphael("gauge");
$(document).ready( function () {
redraw();
});
function redraw() {
//Add a Arrow and line
var rect = gaugeSvg.path('M 0 0 L 40 -34 L 40 -14 L 80 -14 L 80 14 L 40 14 L 40 34 Z');
rect.attr({
"stroke": "black",
"fill" : "black",
"enable" : "true",
}).translate(left + width, goalY);
var txt = gaugeSvg.path('M 0 0 L ' + width + " 0");
txt.attr({
"stroke": "black",
"stroke-width": 12,
"stroke-dasharray": "-",
"stroke-linecap": "round"
}).translate(left, goalY);
//Create a set so we can move the
//arrow and line at the same time
var g = gaugeSvg.set();
g.push(rect, txt);
// var g = gaugeSvg.set(rect, txt);
var me = this,
lx = 0,
ly = 0,
ox = 0,
oy = 0,
moveFnc = function(dx, dy) {
this.translate(dx-ox, dy-oy);
ox = dx;
oy = dy;
},
startFnc = function() {},
endFnc = function() {
ox = lx;
oy = ly;
};
g.drag(moveFnc, startFnc, endFnc);
}
I have'nt used Rapheal . But i have achieved this Drag functionlaity with SVG amd Javascript.
In order to move multiple elements, you need to group them. Means put these elements in 'g' group and then apply drag function on this 'g'. and once finished, you need to 'ungroup' them.
you can see demo here http://jsfiddle.net/rehankhalid/5t5pX/
var mainsvg = document.getElementsByTagName('svg')[0];
function mousemove(event) {
var svgXY = getSvgCordinates(event);// get current x,y w.r.t to your svg.
dx = svgXY.x - mx;// mx means x cordinates of mouse down
dy = svgXY.y - my;
draggroup.setAttribute('transform', 'translate(' + dx + ',' + dy + ')');
}
function getSvgCordinates(event) {
var m = mainsvg.getScreenCTM();
var p = mainsvg.createSVGPoint();
var x, y;
x = event.pageX;
y = event.pageY;
p.x = x;
p.y = y;
p = p.matrixTransform(m.inverse());
x = p.x;
y = p.y;
x = parseFloat(x.toFixed(3));
y = parseFloat(y.toFixed(3));
return {x: x, y: y};
}
I know raphael can create paths, but not lines. I know d3 can create both.
I would like to create a box and whisker chart, similar to this one, but horizontal instead of vertical. I have json data in the form:
{
"lowestValue":"53",
"lowerQuartile":"63",
"medianValue":"73",
"upperQuartile":"80",
"highestValue":"99",
"targetValue":"80"
},
...
How can I create a (or several) box and whisker plot(s) with d34raphael or with pure raphael, so that it will display properly in IE7/IE8?
Here is a picture of the end goal:
The path is such a similar primitive that it seems like it would be easy to recreate such a graph using raw Raphael (which seems increasingly to be my preference these days). Consider such a utility function as this:
function whisker( paper, x, y, width, height, data )
{
var x1 = x + data.lowestValue * width / 100, x2 = x + data.highestValue * width / 100;
var outer_range = paper.path( [ "M", x1, y + height * 0.25, "L", x1, y + height * 0.75, "M", x1, y + height / 2, "L", x2, y + height / 2, "M", x2, y + height / 4, "L", x2, y + height * 0.75 ] ).attr( { fill : 'none', stroke: 'gray' } );
var inner_range = paper.rect( x + ( width * data.lowerQuartile / 100 ), y, width * ( data.upperQuartile - data.lowerQuartile ) / 100, height, 0 ).attr( { fill: 'lightgray', stroke: 'black' } );
var median = paper.path( [ "M", x + width * data.medianValue / 100, y, "L", x + width * data.medianValue / 100, y + height ] ).attr( { fill: 'none', stroke: 'black' } );;
var target = paper.circle( x + ( width * data.targetValue / 100 ), y + height / 2, height / 4 ).attr( { fill: 'black' } );
}
The sixth parameter is simply your json data. You would need to increment the y value for each whisker, of course. Here's the code in action on my website.