I am trying to switch from static html SVGs (with dynamic values) to dynamic d3 SVGs in a Backbone view. I am currently using templates (and would prefer to keep it that way for some of the other properties), but don't have to (as I can refactor those properties into their own view with template).
Anyone have a clean quick example, like with just a circle ?
The closest version of Backbone and d3 I found is here, but this is what I want to get to, and don't have enough d3 experience yet to understand the function calls and structure yet.
The code that is currently providing the problem is in the render() of the View :
var pathFunction = d3.svg.line()
.x(function (d) {return d.x;})
.y(function (d) {return d.y;})
.interpolate("basis"); // bundle | basis | linear | cardinal are also options
//The Circle SVG Path we draw
var svgContainer = d3.select('#measure'+measure.cid);
var circle = svgContainer.append("g")
.append("path")
.data([circleStates[0]])
.attr("d", pathFunction)
.attr("class", "circle");
var compiledTemplate = _.template(this.representations[this.currentRepresentation], measureTemplateParamaters);
$(this.el).find('.addMeasure').before( compiledTemplate );
basically I am trying to draw a circle with a path, defined by already computed points. I just don't know how to get it passed to either the template or the DOM via the Backbone.View
Console Error when "Bead" is selected on this page:
Error: Problem parsing d="function line(data) {
var segments = [], points = [], i = -1, n = data.length, d, fx = d3_functor(x), fy = d3_functor(y);
function segment() {
segments.push(" jquery.js:6326
jQuery.extend.clean jquery.js:6326
jQuery.buildFragment jquery.js:6165
jQuery.fn.extend.domManip jquery.js:5975
jQuery.fn.extend.before jquery.js:5795
(anonymous function) measuresView.js:227
_.each._.forEach underscore.js:78
Backbone.View.extend.render measuresView.js:133
Backbone.View.extend.changeMeasureRepresentation measuresView.js:90
triggerEvents backbone.js:96
Backbone.Events.trigger backbone.js:187
Backbone.View.extend.cycle wholeMeasureRepresentationView.js:46
jQuery.event.dispatch jquery.js:3059
elemData.handle.eventHandle jquery.js:2677
This is the full error , and doesn't match my code, leading me to believe this seems like it is trying to take d3's function and render that, not what I expect d3 to return. The stack trace eventually leads me back to the compiledTemplate that gets passed in ((anonymous function) measuresView.js:227), and that is what I am trying to figure out, How to pass the d3 SVG into the template.
The issue your having is in the bead template. In the template you reference the property pathFunction, which is a function, where you should be referencing the return value of that function. Just change pathFunction to pathFunction() and you should be fine assuming that the pathFunction is written to return an svg path without any arguments. If that is the case, here is the way the template should look:
<div id="measure<%= measure.cid %>" class="measure">
<div class="btn" id="a">Unroll</div>
<div class="btn" id="b">Rollup</div>
<span class="title">Measure <span class="number"><%= measureCount %></span>
<% if(measureCount == 1) { %>
<% } else { %>
- <span class="delete">[X]</span>
<% } %>
</span>
<svg id="svg<%= measure.cid %>" xmlns="http://www.w3.org/2000/svg" version="1.1" class="circular-pie">
<path d="<%= pathFunction() %>" />
<!-- <circle cx="<%= cx %>" cy="<%= cy %>" r="<%= measureR %>" stroke="black" stroke-dasharray="1,3" stroke-width="1" fill="none" /> -->
<g id="<%= beatHolder %>">
</g>
</svg>
</div>
As far as what you are trying to achieve though, I think you what you want to do is to render an SVG element as part of your template, then after appending the HTML to the DOM, use the SVG element as the root of your visualization, so:
// This should be in your view's render function
// Render the template
var compiledTemplate = _.template(this.representations[this.currentRepresentation], measureTemplateParamaters);
// Insert the html into the DOM
d3.select('#someContainer').html(compiledTemplate);
// Then draw your visualization
var pathFunction = d3.svg.line()
.x(function (d) {return d.x;})
.y(function (d) {return d.y;})
.interpolate("basis");
//The Circle SVG Path we draw
var svgContainer = d3.select('#measure'+measure.cid);
var circle = svgContainer.append("g")
.append("path")
.data([circleStates[0]])
.attr("d", pathFunction(/*you'll need to insert some coordinate
information here see http://www.dashingd3js.com/svg-paths-and-d3js*/))
.attr("class", "circle");
// Do some other stuff
I think it's because you're using the same string delimiter " for your xml and inside your function. JQuery tries to parse your compiled template and fails to evaluate the function in the 'd' attribute that stops at ...segments.push(.
Change segments.push("M" into segments.push('M' and segments.join("") into segments.join('')and you should be fine.
Related
I am creating a filter that is intended to change weather id code with corresponding image.
HTML looks like this:
<span ng-bind-html="weather.today.weather[0].id | weatherIcon"></span>
When I use img tag it works just fine:
.filter('weatherIcon', function() {
return function() {
var template = `<img src="img/weather-icons-set/CLOUDS/CLOUDS/001lighticons-02.svg">`;
return template;
}
})
But I would like to embed my svg to be able changing colors etc. Unfortunately with object tag it doesn't work at all:
.filter('weatherIcon', function() {
return function() {
var template = `<object type="image/svg+xml" data="img/weather-icons-set/CLOUDS/CLOUDS/001lighticons-02.svg" width="100" height="100"></object>`;
return template;
}
})
I've also tried to put ng-include in filter return, but it also failed. Can you tell me, what is wrong with returning <object> in a filter, or give me a hint for another approach?
Your SVG image might define fill or stroke properties and it might be the cause of your problem, you should check first inside your file.
Otherwise I'll just give you an hint for another approach I'm using. Just note that you'll need to modify your SVG images.
You can use the nodes <use> and <symbol> in SVG (Use, Symbol).
Instead of your span:
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 1792 1792">
<use xlink:href="{{PATH_TO_YOUR_FILE + ID}}" style="fill: YOUR_COLOR;"></use>
</svg>
The subtility is here: xlink:href="{{PATH_TO_YOUR_FILE + ID}}". The SVG image you use must define an Id and be inside a <symbol> node.
e.g.:
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="myId" viewBox="0 0 1792 1792">
[...img here...]
</symbol>
</svg>
Then you'll use it like this in your <use> node: xlink:href="img/weather-icons-set/CLOUDS/CLOUDS/001lighticons-02.svg#myId"
Make sure your images don't override the properties fill or stroke
Ultimately I used a web-font instead. My filter looks like this:
.filter('weatherIcon', function($sce) {
return function(weatherCode, isNight) {
var template;
var iconCode;
...
template = `<span class="icon" data-icon="${iconCode}"></span>`
return $sce.trustAsHtml(template);
}
})
I can see the svg elements in the chrome inspector, but there's nothing on the page. The svg element is visible, but none of the rect elements inside show up.
<div class="chart-area">
<svg width="750" height="263">
<g transform="translate(0,0)">
<rect y="0" height="263" width="150"></rect>
</g>
<g transform="translate(187.5,0)">
<rect y="263" height="0" width="150"></rect>
</g>
<!-- ... -->
</svg>
</div>
I tried adding a namespace and a viewBox to the svg with no luck. What am i missing here?
Edit - Angular
The svg is ok. If i copy & paste the svg elements outside of the directive it renders correctly. However generating the svg inside the directive link function breaks it.
<div class="chart">
<svg></svg>
</div>
The rendered markup is identical tho, so still very lost.
Edit #2 - Namespace
Based on AmeliaBR's comment, is there a way to explicitly set the namespace? I've tried to put the namespace in markup, but that doesn't help. I was under the impression html5 didn't need the namespace, but perhaps i'm wrong?
<!-- doesnt work -->
<svg xmlns="http://www.w3.org/2000/svg"></svg>
Edit #3 - Wtf?
Robert Longson says there's no namespace in html markup, only xhtml, so that can't be it. So the question remains, why does it work when specified in markup, but not work when added via javascript?
Edit #4 - link function
The angular link function that generates the svg. Full plunker here.
function link (scope, el) {
// munge scope data
var width = el.find('.chart').width()
var height = Math.floor(width / RATIO)
console.log(width, height)
// 750 421
var svg = d3.select(el.find('svg')[0])
.attr('width', width)
.attr('height', height)
// x, y
var bar = svg.selectAll('g')
.data(data)
.enter().append('g')
.attr('transform', function (d) { return 'translate(' + x(d.x) + ',0)' })
bar.append('rect')
.attr('y', function (d) { return y(d.y) })
.attr('height', function (d) { return height - y(d.y) })
.attr('width', function () { return width / data.length })
.attr('fill', 'black')
}
Edit #5 - I give up
The isolated Plunker (http://plnkr.co/edit/7dlqNz28HjX3Ho8vvn6m?p=preview) seems to work, so there's something whack happening with my code (literally copy/pasted). I give up!
Gonna update all dependencies, clear the cache, try on different browsers, etc. Feel free to close this :(
Make sure you have a stroke and/or fill color defined.
The rectangle is invisible because its height is zero. Set the height-attribute to some value above 0;
I'm generating some <rect>s with <animate> children using the ng-repeat directive.
On page load the animations are correctly triggered and everything happens as expected.
However, when I add a new <rect> the animation does not occur.
The following code snippet demonstrates this behaviour:
function Controller($scope) {
$scope.rects = [];
var spacing = 5;
var width = 10;
var height = 100;
var x = 10;
var y = 10;
$scope.newRect = function() {
$scope.rects.push({
x: x,
y: y,
width: width,
height: height
});
x += spacing + width;
};
for (var i = 0; i < 5; i++) {
$scope.newRect();
}
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app ng-controller="Controller">
<svg width="1000" height="200">
<rect ng-repeat="rect in rects" x="{{rect.x}}" y="{{rect.y}}" height="{{rect.height}}" width="{{rect.width}}">
<animate attributeName="y" from="{{rect.height}}" to="{{rect.y}}" dur="1s" repeatCount="1" />
<animate attributeName="height" from="0" to="{{rect.height}}" dur="1s" repeatCount="1" />
</rect>
</svg>
<button ng-click="newRect()">One more</button>
</div>
Upon loading the example, 4 <rect> will appear, animating from the bottom to the top. However, when pressing the "One more" button, the new <rect> will be added without animation (behaviour tested on Firefox 35 and Chrome 38).
How can I trigger the animation for the new <rect>s?
The default begin time for an animation element (equivalent to begin="0s") is always measured relative to the SVG load time. This is true even if you create the animation element dynamically after page load.
If you want any other begin time, you will need to either (a) explicitly set a different value for the begin attribute, or (b) use the beginElement() or beginElementAt(offsetTime) methods of the animation element DOM objects. Since you're creating the elements with scripts and you want them to start right after inserting them, the beginElement() method is probably the easiest approach.
Edited to add:
If beginElement isn't working because angular-js doesn't provide you with direct access to the created element, you can make use of the event format for the begin attribute. If the begin attribute contains the name of a DOM event (or a semi-colon separated list of times and event names), the animation will begin when that event occurs. By default, the event will be listened for on the element that the animation will affect—the rectangle in your case. (You can tie the animation to a different element using the elementID.eventName format).
The trick to get the animation to start as soon as it is added is to link it to one of the rarely used DOM-mutation events, specifically DOMNodeInserted. That way, when you add the animation node as a child of the <rect>, or when you add the <rect> to the SVG, the event and then the animation will be triggered immediately.
If you want a delay between inserting the element and triggering the animation, use the eventName + timeOffset format.
Here is the proof of concept with vanilla JS (as a fiddle).
And here is your modified snippet; the JS code is the same, only the angular template has changed:
function Controller($scope) {
$scope.rects = [];
var spacing = 5;
var width = 10;
var height = 100;
var x = 10;
var y = 10;
$scope.newRect = function() {
$scope.rects.push({
x: x,
y: y,
width: width,
height: height
});
x += spacing + width;
};
for (var i = 0; i < 5; i++) {
$scope.newRect();
}
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app ng-controller="Controller">
<svg width="1000" height="120">
<rect ng-repeat="rect in rects" x="{{rect.x}}" y="{{rect.y}}"
height="{{rect.height}}" width="{{rect.width}}">
<animate attributeName="y" from="{{rect.height}}" to="{{rect.y}}"
dur="1s" repeatCount="1" begin="DOMNodeInserted"/>
<animate attributeName="height" from="0" to="{{rect.height}}"
dur="1s" repeatCount="1" begin="DOMNodeInserted"/>
</rect>
</svg>
<button ng-click="newRect()">One more</button>
</div>
I'm not sure whether you intentionally want to have the bars start 10px offset from the bottom line. If that was accidental you can fix it by setting the from value of the first animation to rect.height + rect.y.
I've tested the fiddle in the latest Chrome and Firefox. If you need to support older browsers, especially if you're supporting older IE with a SMIL polyfill, you'll want to test to make sure the DOM mutation events are being thrown correctly.
I've just started learning AngularJS and I'm approaching directives. I'd need to create a directive named f.e. "thumbnail" which takes as input a "src" (the image) and create a thumbnail of it.
So far I've coded the javascript function to create the thumbnail which does its job correctly:
function readURL(input) {
var imageSize = 50;
if (input.files && input.files[0]) {
var reader = new FileReader();
reader.onload = function (e) {
var blah = $('#myimage');
blah.attr('src', e.target.result);
var height = blah.height();
var width = blah.width();
if (height < imageSize && width < imageSize) {
if (width > height) {
blah.width(imageSize);
}
else {
blah.height(imageSize)
}
}
};
reader.readAsDataURL(input.files[0]);
}
}
</script>
<body onload="readURL(this);">
<img id="myimage" src="../picture.gif" width="50" alt="your image" />
However I have not been able to find a simple example which produces (in AngularJS) an Element, based on a JS function. Any help ?
Thanks!
It depends on what you want to achieve. If your purpose of creating a thumbnail at client-side is to:
Upload in a smaller size in a web application: perhaps you can make use of a library like http://www.plupload.com.
Display only: you should just use CSS resizing with width and height properties.
<img src="/original/320x320.jpg" style="width: 100px; height: 100px">
Reading/storing in an Ionic/PhoneGap app: try make use of angular-thumbnail (disclaimer: I wrote it)
Doing image manipulation of this kind on the client side is not a very good idea , because, as of now there isn't a good support for Filereader in IE.
You can delegate this either to server(you will find lot of node module for image manipulation) or you can use external services like cloudinay
I have two directives, my-svg and my-rect. I want to use them like this:
<svg my-svg>
<my-rect/>
</svg>
my-rect creates an SVG rect and my-svg creates an svg node with the transcluded rectangle inside. In the end, what I want to get is:
<svg width='300' height='300'>
<rect x="140" y="30" width="25" height="25" fill="red"></rect>
</svg>
See example here: http://plnkr.co/edit/UIyUtX?p=preview
As you can see, the red rectangle isn't displayed, even though it exists in the DOM. According to this discussion, it seems that the rectangle isn't displayed because it is an HTMLElement when it should be an SVGElement.
As suggested in that same discussion, I'm using a custom directive compiler to transform the DOM nodes from type HTMLElement to SVGElement, but even that doesn't seem to work in my use-case.
What am I doing wrong?
Thanks
Under the hoods AngularJS uses JQuery or JQLite to create elements from templates to replace with.
JQuery and JQLite both use document.createElement rather than document.createElementNS with the correct SVG namespace.
In your directive you need to take over the creation of SVG elements from AngularJS.
You can inject the following helper function into your directive:
.value('createSVGNode', function(name, element, settings) {
var namespace = 'http://www.w3.org/2000/svg';
var node = document.createElementNS(namespace, name);
for (var attribute in settings) {
var value = settings[attribute];
if (value !== null && !attribute.match(/\$/) && (typeof value !== 'string' || value !== '')) {
node.setAttribute(attribute, value);
}
}
return node;
})
And make use of it in the link function rather than using a template (either external or inline) - something like:
link: function(scope, element, attrs) {
var cx = '{{x}';
var cy = '{{y}}';
var r = '{{r}}';
var circle = createSVGNode('circle', element, attrs);
angular.element(circle).attr('ng-attr-cx', cx);
angular.element(circle).attr('ng-attr-cy', cy);
angular.element(circle).attr('ng-attr-r', r);
element.replaceWith(circle);
$compile(circle)(scope);
}
You can see an example of this working - in a piechart context - over at https://github.com/mjgodfrey83/angular-piechart/.
A fix landed in angular 1.3.0-beta8 to allow non html directive template types to be specified - see here. For an example of it being used check out angular-charts.
Hope that helps.
Putting
<g>
<my-rect></my-rect>
</g>
will display the rectangle.
It doesn't answer the question what are you doing wrong, but it does get the code to display what you want. I spent some time looking at this problem myself but I could not get it to work , and so solved the problem in a different way. What is the problem you are trying to solve using this method?