I'm trying to attach Javascript events to SVG elements using code that is inside the "module pattern". I'm very new to Javascript, as I'm sure will be evident by the code below. I've attempted to recreate the situation using a much simpler version of the code I am actually using. Ironically, I am actually further away from getting it to work with this example, but my brain hurts from attempting to get it to work and researching elsewhere on this site. So, I am reaching out to all the clever people out there to see if they can help. The problem I am having (when I get it to work!) is that the Javascript events do not appear to be binding to the DOM elements and just firing on page load. When I then fire the event again (by moving the mouse) I get a Javascript error caused by the D3 library:
"Uncaught TypeError: n.apply is not a function"
If anyone can look past the rushed effort at recreating a module pattern and get this example to work - hopefully I will be able to incorporate the same into my actual code (the only real difference between the example and real code is that there are a bunch of "extender" modules rather than everything being in one).
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8"></script>
</head>
<body>
<div id="box1"></div>
<div id="box2"></div>
<script>
function mouseOver(i) {
return this.targets[i].select("#" + this.targets[i].container).select("circle").attr("fill", "blue");
}
function mouseOut(i) {
return this.targets[i].select("#" + this.targets[i].container).select("circle").attr("fill", "green");
}
var example = (function () {
return {
addRect: function addRect(container) {
this.box = d3.select("#" + container).append("svg");
this.rect = this.box.append("rect")
.attr({
"fill": "red",
"width": 50,
"height": 50,
});
},
addCircle: function addControl(targets) {
this.targets = targets
for (target in this.targets) {
tt = this.targets[target]
tt.circle = tt.box.append("circle")
.attr({
"fill": "green",
"cx": -10,
"cy":-10,
"r": 10,
"transform": "translate(30,30)"
})
};
},
addControl: function addControl(targets) {
this.targets = targets
for (target in this.targets) {
tt = this.targets[target]
tt.control = tt.box.append("rect")
.attr({
"fill": "none",
"width": 50,
"height": 50,
})
.on("mouseover", mouseOver(target))
.on("mouseout", mouseOut(target))
};
},
};
})();
var first = new example.addRect("box1");
var second = new example.addRect("box2");
var circs = new example.addCircle([first, second]);
var controls = new example.addControl([first, second]);
</script>
</body>
</html>
Problem1:
The event gets fired on page load
Reason:
You are doing
.on("mouseover", mouseOver(target)) //this is wrong the second parameter should have been a function.
something like this
.on("mouseover", function(){mouseOver(target)})
Problem 2:
tt.control = tt.box.append("rect")
.attr({
"fill": "none",//this should not be none else mouse over will not work as you expect
"width": 50,
"height": 50,
})
.on("mouseover", mouseOver(target))
.on("mouseout", mouseOut(target))
It should be done with opacity 0 so that its not visble
for (target in this.targets) {
tt = this.targets[target]
tt.control = tt.box.append("rect")
.attr({
"fill": "blue",
"width": 50,
"height": 50,
"opacity": 0
})
Problem 3:
You can set the parameter to the DOM rather than passing it in the function.
Something like this
for (target in this.targets) {
tt = this.targets[target]
tt.control = tt.box.append("rect")
.attr({
"fill": "blue",
"width": 50,
"height": 50,
"opacity": 0
})
.on("mouseover", function() {
mouseOver(tt)
})
.on("mouseout", function() {
mouseOut(tt)
});
tt.control.node().dataset = tt;//binding the data to the dom
};
And in the mouse event get the bound object like this:
function mouseOver(target) {
//gettting the data from the dom
event.target.dataset.circle.attr("fill", "blue");
}
function mouseOut(target) {
event.target.dataset.circle.attr("fill", "green");
}
Working code here.
Related
The Plunker example
In the example above you can see that the line is rendered under the orange area graph:
Was reading this trend here, then I tried this d3.select('svg#chart .lines1Wrap').moveToFront();, but got the error moveToFront is not a function.
Chart code
var data = [{
"key": "Price",
"type": "line",
"yAxis": 2,
"values": [
[1443621600000, 71.89],
[1443619800000, 75.51],
[1443618000000, 12.49],
[1443616200000, 20.72],
[1443612600000, 70.39],
[1443610800000, 59.77],
]
}, {
"key": "Quantity1",
"type": "area",
"yAxis": 1,
"values": [
[1136005200000, 1],
[1138683600000, 5],
[1141102800000, 10],
[1143781200000, 0],
[1146369600000, 1],
[1149048000000, 0],
]
}];
data = data.map(function(series) {
series.values = series.values.map(function(d) {
return {
x: d[0],
y: d[1]
}
});
return series;
});
nv.addGraph(function() {
var chart = nv.models.multiChart()
.margin({
top: 20,
right: 40,
bottom: 50,
left: 40
})
.yDomain1([0, 10])
.yDomain2([0, 100]) // hard-coded :<
.interpolate("linear") // don't smooth out the lines
.color(d3.scale.category10().range());
chart.xAxis.tickFormat(function(d) {
return d3.time.format('%I:%M')(new Date(d));
});
chart.yAxis1.tickFormat(d3.format(',f'));
chart.yAxis2.tickFormat(function(d) {
return '$' + d3.format(',f')(d)
});
d3.select('svg#chart')
.datum(data)
.transition().duration(500).call(chart);
d3.selection.prototype.moveToFront = function() {
return this.each(function() {
this.parentNode.appendChild(this);
});
};
d3.select('svg#chart .lines1Wrap').moveToFront();
chart.update();
nv.utils.windowResize(chart.update);
return chart;
});
UPDATE
Found this answer here, tried to use the solution:
d3.selection.prototype.moveToFront = function() {
return this.each(function() {
this.parentNode.appendChild(this);
});
};
d3.selection.prototype.moveToBack = function() {
return this.each(function() {
var firstChild = this.parentNode.firstChild;
if (firstChild) {
this.parentNode.insertBefore(this, firstChild);
}
});
};
d3.select('svg#chart .lines1Wrap').moveToFront();
d3.select('svg#chart .nv-areaWrap').moveToBack();
No more error, however the blue line graph still is not moved in front of all the others.
Add this to move your line(line chart) DOM element after the line(Area Chart) DOM element.
d3.select('.lines2Wrap').node().parentNode.insertBefore(d3.select('.stack1Wrap').node(), d3.select('.lines2Wrap').node());
Full working code here
Hoe this helps! :)
As I struggled with a similar problem for two days I will post my solution here in case it helps someone.
In my chart settings I have dispatch function which will append a rectangle to the chart and then lower it to the bottom:
dispatch: {
renderEnd: () => {
drawThresholds();
lowerThresholdAreas();
}
}
In the drawThresholds I draw the actual rectangle:
const drawThresholds = () => {
try {
let svgContainer = d3.select(`svg g`);
svgContainer.append('rect')
.attr('x', 0)
.attr('y', 2)
.attr('width', 200)
.attr('height', 200)
.attr('class', 'threshold_area')
.attr('fill', 'yellow');
}
catch (err) {
// Selector throws an error instead of returning empty list
// when element is not found.
// Ignore exceptions, they occur because page is not loaded yet.
}
};
And finally after rectangles are appended, I need to place them to the bottom by calling lowerFunction. use d3 selectAll to get all .threshold_area classes and then insert them before the line.
const lowerThresholdAreas = () => {
d3.selectAll('.threshold_area').each(function () {
this.parentNode.insertBefore(this, this.parentNode.firstChild);
});
};
And what is happening, is that on SVG canvas there is no z-index. It means the order of appends defines the layers, meaning if you append the rectangle as last item, it will be on top. So the order of elements needs to be changed.
Also a step where I did mistake was that I first used ES6 function declaration and it doesn't work with "this" as I supposed it would.
Read more about selection: https://github.com/d3/d3-selection/blob/master/README.md#select
Read more about lowering or raising items: https://github.com/d3/d3-selection/blob/master/README.md#selection_lower
Here is a working plunker for seeing it in action: https://plnkr.co/edit/QI2HcxcJYRAv0FzXWihd?p=preview
I think it's just that on your particular graph, the line series is under lines2wrap, not lines1wrap. IIRC, this has to do with which series is 'first'. In your plunker, I changed the 1 to a 2 and it worked.
It's a hacky workaround (that's mine from Github, thanks for notifying me), and probably requires some tinkering to be more generally useful.
I have a frameless window with a titlebar set up much like in the node-webkit sample apps (that is, with buttons that have -webkit-app-region: no-drag; so that they accept user input).
Because I'd like my app to be completely themeable, I decided to implement the minimize/maximize/close buttons as SVG and manipulate their color/opacity through JavaScript.
Example
As an example, I have the following close button close.svg:
<svg class="close-icon" xmlns="http://www.w3.org/2000/svg" width="30" height="26">
<g>
<title>Close</title>
<path class="close-icon-bg" fill="#c8c8c8" d="M0 0h30v26h-30z"/>
<path class="close-icon-fg" d="M11.766 8l-1.766 1.766 3.221 3.221-3.221 3.247 1.766 1.766 3.221-3.247 3.247 3.247 1.766-1.766-3.247-3.247 3.247-3.221-1.766-1.766-3.247 3.221-3.221-3.221z"/>
</g>
</svg>
I then have the following in my titlebar div:
<object id="close-btn" data="img/close.svg" type="image/svg+xml"></object>
Then in JavaScript I have the following:
function refreshTheme(theme) {
var but2 = {
"default": {
"bg": {"color": "#000000", "opacity": "0.0"},
"fg": {"color": "#000000", "opacity": "0.2"}
},
"hover": {
"bg": {"color": "#ac4142", "opacity": "1.0"},
"fg": {"color": "#e0e0e0", "opacity": "1.0"}
},
"click": {
"bg": {"color": "#000000", "opacity": "1.0"},
"fg": {"color": "#e0e0e0", "opacity": "1.0"}
}
};
createButton("close", but2, dummy);
}
function createButton(name, theme, func) {
var svgobj = document.getElementById(name + "-btn"), svg = svgobj.contentDocument;
styleIcon(svg, name, theme.default);
svgobj.onmouseover = function() {
styleIcon(svg, name, theme.hover);
};
svgobj.onmouseout = function() {
styleIcon(svg, name, theme.default);
};
svgobj.onmousedown = function() {
styleIcon(svg, name, theme.click);
};
svgobj.onclick = function() {
func();
};
}
function styleIcon(icon, name, theme) {
var bg = icon.getElementsByClassName(name + "-icon-bg")[0];
bg.setAttribute("fill", theme.bg.color);
bg.setAttribute("fill-opacity", theme.bg.opacity);
var fg = icon.getElementsByClassName(name + "-icon-fg")[0];
fg.setAttribute("fill", theme.fg.color);
fg.setAttribute("fill-opacity", theme.fg.opacity);
}
Essentially what happens is, I have a hard-coded JSON object but2 encapsulating the button states, which I pass to createButton(), which is responsible for setting up the various mouse events. I can confirm that the onmouseover and onmouseout events work well:
The top screenshot shows onmouseout, the bottom onmouseover.
The Problem
What doesn't work are the onmousedown and onclick events! They appear to be set up correctly in the debug window and no errors are present in the console, but they never get fired.
I was under the impression that <object> tags accept all standard HTML events, so this should work. Is there something I'm missing? Some node-webkit caveat maybe? Something to do with SVG's?
How can I get the onmousedown and onclick events to fire properly?
It turns out this is an actual problem with SVG's in browsers. The solution, as noted here, is to have the SVG hold the mouse up/down/click events in a transparent (fill-opacity="0.0") rectangle spanning the whole image.
In my case, the solution was:
var svg_click = svg.getElementsByClassName(name + "-icon-click")[0];
svg_click.onmousedown = function() {
styleIcon(svg, name, theme.click);
};
// etc...
I'm not terribly pleased with it, considering it should also work from the <object> tag, but hey...it's still a solution.
when I click the red circle, the window will popup a alert once.
It works fine in firefox and chrome, but in ie8, it's popup alerts twice.
How could I fix it?
please see my code in the following:
Raphael("world", 1000, 400, function () {
var r = this;
r.rect(0, 0, 1000, 400, 0).attr({
stroke: "none",
fill: "0-#9bb7cb-#adc8da"
});
var click = function(){
alert(this.type);
};
r.setStart();
var hue = Math.random();
for (var country in worldmap.shapes) {
r.path(worldmap.shapes[country]).attr({stroke: "#ccc6ae", fill: "#f0efeb", "stroke-opacity": 0.25});
}
var dot = r.circle(772.9870633333333, 166.90446666666668).attr({
title: "Point",
fill: "red",
stroke: "#fff",
"stroke-width": 2,
r: 5
});
var world = r.setFinish();
world.click(click);
});
I've had this problem a few time. Solved it by using a mouse up event instead of a click event. IE sucks.
I find a way to fix this issue, replace the Raphael Set Object with a array Object to push all Rahpael Elements Object, then loop the array to add click event for each Element.
see the following code:
var set = [];
// r.setStart();
for (var country in worldmap.shapes) {
var element = r.path(worldmap.shapes[country]).attr({stroke: "#ccc6ae", fill: "#f0efeb", "stroke-opacity": 0.25});
set.push(element);
}
var dot = r.circle(772.9870633333333, 160.90446666666668 , 5).attr({
title: "Point",
fill: "red",
stroke: "#fff",
"stroke-width": 2
});
set.push(dot);
for(var i = 0; i < set.length; i++){
var element = set[i];
element.click(click);
}
// var world = r.setFinish();
// world.click(click);
I had a similar problem in Firefox, although what I was doing was completely different.
The way I solved it was to prevent the event from firing twice in too short of an interval.
I would use this function:
function rateLimit(func) {
var lastcall = func.lastcall || 0,
now = new Date().getTime();
if( now-lastcall < 250) return false;
func.lastcall = now;
return true;
}
Then in the function I want to limit from firing too often, I can do this:
if( !rateLimit(arguments.callee)) return false;
However, you might have a small issue if you are using alert(), since that will completely block execution and the second run will still fire. I would strongly suggest using console.log() instead of alert() to keep track of values, as this will avoid interrupting the flow of the program (especially when you get into asynchronous stuff, you can get real mysteries if you stop things with an alert)
Hope this helps!
I apologize for asking this question but I am just looking for a little guidance on this morning. I simply want to create a function so that way I can make a Raphael element glow by just passing in that element. Below is the code I have. Why does this not work?
var paper = Raphael("playarea", 500, 500);
var rectangle = paper.rect(100, 100, 200, 200, 4);
function elemHover(var el)
{
el.hover(
// When the mouse comes over the object //
// Stock the created "glow" object in myCircle.g
function() {
this.g = this.glow({
color: "#0000EE",
width: 10,
opacity: 0.8
});
},
// When the mouse goes away //
// this.g was already created. Destroy it!
function() {
this.g.remove();
});
}
elemHover(rectangle);
here is the fiddle http://jsfiddle.net/aZG6C/15/
You should fill the element( rectangle in our case) to trigger the hover.
rectangle.attr("fill", "red");
Try this fiddle http://jsfiddle.net/aZG6C/17/
The full code will look like
<div id="playarea"></div>
<script type="text/javascript">
var paper = Raphael("playarea", 500, 500);
var rectangle = paper.rect(100, 100, 200, 200, 4);
function elemHover(el)
{
el.hover(
// When the mouse comes over the object //
// Stock the created "glow" object in myCircle.g
function() {
this.g = this.glow({
color: "#0000EE",
width: 10,
opacity: 0.8
});
},
// When the mouse goes away //
// this.g was already created. Destroy it!
function() {
this.g.remove();
});
}
rectangle.attr("fill", "red");
elemHover(rectangle);
</script>
Update
Hover event is triggered only if the element is filled with something. If you want to have a transparent element you can try
rectangle.attr("fill", "transparent");
Check the fiddle here http://jsfiddle.net/aZG6C/20/
I have the following piece of code in javascript that basically hide or show a Raphaeljs set when I click on it. It works perfectly well under Google Chrome, FireFox and Safari, but not at all under Internet Explorer.
var paper = Raphael(document.getElementById('ADiv'), 450, 490);
var group = paper.set();
var toxicRect = paper.rect(0, 0, 120, 60, 10 );
toxicRect.attr({"stroke-width": 1,
"stroke" : "#3083BE",
"fill" : "#D1DFE9"});
group.push( toxicRect );
var toxicRectText = paper.text(60, 25, "Toxic in air\nthrough inhalation");
toxicRectText.attr({"font-size": 12 });
group.push( toxicRectText );
var toxicToConcentrationPath = paper.path("M60 60L60 80")
toxicToConcentrationPath.attr({"stroke-width": 1,
"stroke" : "#3083BE"});
group.push( toxicToConcentrationPath );
var concentrationRect = paper.rect(0, 80, 120, 60, 10 );
concentrationRect.attr({"stroke-width": 1,
"stroke" : "#3083BE",
"fill" : "#D1DFE9"});
group.push(concentrationRect);
var conRectText = paper.text( 60, 105, "Is concentration\n> TLV/TWA");
conRectText.attr({"font-size": 12});
group.push(conRectText);
var conRectTextYes = paper.text(50, 135, "Yes / ");
conRectTextYes.attr({"font-size": 12,
"font-style": "italic"});
group.push(conRectTextYes);
var conRectTextNo = paper.text(75, 135, "No");
conRectTextNo.attr({"font-size": 12,
"font-style": "italic"});
group.push(conRectTextNo);
var monitorConcentrationGroup = paper.set();
var monitorConcentrationRect = paper.rect(140, 95, 60, 30, 10 );
monitorConcentrationRect.attr({"stroke-width": 1,
"stroke" : "#3083BE",
"fill" : "#D1DFE9"});
monitorConcentrationGroup.push(monitorConcentrationRect);
var monitorConcentrationRectText = paper.text(170, 115, "Monitor");
monitorConcentrationRectText.attr({"font-size": 12});
monitorConcentrationGroup.push(monitorConcentrationRectText);
var concentrationToMonitorPath = paper.path("M120 110L140 110")
concentrationToMonitorPath.attr({"stroke-width": 1,
"stroke" : "#3083BE"});
monitorConcentrationGroup.push(concentrationToMonitorPath);
monitorConcentrationGroup.hide();
//Actions when clicking on decisions
conRectTextYes.node.onclick = function () {
monitorConcentrationGroup.hide();
};
conRectTextNo.node.onclick = function () {
monitorConcentrationGroup.show();
};
Anyone has any ideas? You can test it at http://raphaeljs.com/playground.html by making a cut and paste and omitting the first line of the script. Clicking the "No" should make a box appears, at least in Chrome, but not in IE...
Thank you!
Raphael uses VML to render shapes in IE8, specifically the VML textpath element in your example. However, IE8 does not fire mouse events for that element. You could go up the node tree and attach your event handler to the shape element that contains the textpath but, even then, the active-area only consists of the pixels that make up the text, so it's very difficult to click.
A better solution would be to add a transparent rectangle behind the text and attach your event handler to that as well:
...
// Make the rectangle slightly larger and offset it
// from the text coordinates so that it covers the text.
var conRectTextNoContainer = paper.rect(75 - 8, 135 - 9, 17, 14);
// Give the rectangle a fill (any color will do) and
// set its opacity to and stroke-width to make it invisible
conRectTextNoContainer.attr({
fill: '#000000',
'fill-opacity': 0,
'stroke-width': 0
});
group.push(conRectTextNoContainer);
var conRectTextNo = paper.text(75, 135, "No");
conRectTextNo.attr({
"font-size": 12,
"font-style": "italic"
});
group.push(conRectTextNo);
...
var conRectTextNoNode = conRectTextNo.node;
if (conRectTextNoNode.tagName === 'textpath') {
// We're in IE8, attach the handler to the parentNode
conRectTextNoNode = conRectTextNo.node.parentNode;
}
conRectTextNoContainer.node.onclick = (
conRectTextNoNode.onclick = function () {
monitorConcentrationGroup.show();
}
);
...
Working Demo: http://jsfiddle.net/brianpeiris/8CZ8G/show/ (Editable via: http://jsfiddle.net/brianpeiris/8CZ8G/)
roughly similar to another question and I will post the same answer:
onmousedown worked for me in IE 7,8, and 9
st[0].onclick = function () {
myFunc(dataObj);
st.toFront();
R.safari();
};
st[0].onmousedown = function () {
myFunc(dataObj);
st.toFront();
R.safari();
};
I tried some other methods as well, abstracting the function to a variable but it didnt work. In my case I cannot add a rectangle to the display and have people click on this, it was a poor solution for several reasons.
Hope this helps!