Use svg with angularjs ng-repeat - angularjs-ng-repeat

I am learning angularjs and I am trying use ng-repeat to create an svg graph.
I have this html:
<svg>
<g id="g_{{$index}}" ng-repeat="i in range" ng-cloak>
<rect x="{{i / 5}}" y="{{i / 5}}" width="{{i / 5}}" height="{{i / 5}}"></rect>
</g>
</svg>
the 'range' is just a simple array which is defined in controller like this:
$scope.range = [100, 200, 300];
the html is working; the rects are rendered on my page.
However, Chrome keeps throwing the following error:
Error: Invalid value for <rect> attribute height="{{i / 5}}" js/angular.js:1584
JQLiteClone js/angular.js:1584
JQLite.(anonymous function) js/angular.js:2163
publicLinkFn js/angular.js:3862
ngRepeatWatch js/angular.js:13641
Scope.$digest js/angular.js:7889
Scope.$apply js/angular.js:8097
js/angular.js:961
invoke js/angular.js:2857
resumeBootstrapInternal js/angular.js:959
bootstrap js/angular.js:973
angularInit js/angular.js:934
js/angular.js:14756
fire js/jquery-2.0.0.js:2863
self.fireWith js/jquery-2.0.0.js:2975
jQuery.extend.ready js/jquery-2.0.0.js:398
completed js/jquery-2.0.0.js:93
It seems that it does not quite like what I am doing...
Does anyone have an idea why I'm receiving this error?

Markus' comment to use late binding is best.
Ie. prefix your attribute with either 'ng-attr-', 'ng:attr:' or 'ng_attr_', like this:
<rect ng:attr:x="{{i / 5}}" ng:attr:y="{{i / 5}}" ng:attr:width="{{i / 5}}" nng:attr:height="{{i / 5}}"></rect>

The problem is Chrome sees the Angular interpolation str as an invalid value for those attributes (since at least at one time the element actually exists in the DOM -- though invisibly -- with "invalid" values). I have written a solution that is in line with other Angular solutions as to browser handling of special attributes where instead of using x, y, width, and height, you specify ng-x, ng-y, ng-width, and ng-height and the real attributes are set after the values are interpolated.
Here is the solution on JSFiddle. I'm going to submit a patch to Angular to see if we can get this in the core.
HTML
<div ng-app="SampleApp" ng-controller="MainCtrl">
<svg>
<g id="g_{{$index}}" ng-repeat="i in range" ng-cloak>
<rect ng-x="{{i / 5}}" ng-y="{{i / 5}}" ng-width="{{i / 5}}" ng-height="{{i / 5}}"></rect>
</g>
</svg>
</div>
JS
angular.module('SampleApp', [], function() {})
.directive('ngX', function() {
return function(scope, elem, attrs) {
attrs.$observe('ngX', function(x) {
elem.attr('x', x);
});
};
})
.directive('ngY', function() {
return function(scope, elem, attrs) {
attrs.$observe('ngY', function(y) {
elem.attr('y', y);
});
};
})
.directive('ngWidth', function() {
return function(scope, elem, attrs) {
attrs.$observe('ngWidth', function(width) {
elem.attr('width', width);
});
};
})
.directive('ngHeight', function() {
return function(scope, elem, attrs) {
attrs.$observe('ngHeight', function(height) {
elem.attr('height', height);
});
};
});
function MainCtrl($scope) {
$scope.range = [100, 200, 300];
}

Related

Background image not showing on canvas in a strange way

I'm coding a greeting card generator to train in VueJS 3. Everything is working correctly, apart from one thing, look at my code:
<template>
<div>
<h1>greeting card generator</h1>
<div class="board">
<canvas id='myCanvas' :width="size.w" :height="size.h" tabindex='0'
style="border:1px solid #000000;"
></canvas>
</div>
<textarea
:style="'width:' + size.w + 'px; resize:none;'"
v-model="texte"
placeholder="Write your text here">
</textarea>
</div>
</template>
<script>
import {
defineComponent, onMounted, ref, reactive, watch,
} from 'vue';
export default defineComponent({
setup() {
const myCanvas = ref(null);
const texte = ref('');
const rapport = ref(0);
const size = reactive({
w: window.innerWidth * 0.8,
h: (window.innerWidth * 0.8) / 1.8083832335329342,
});
function drawText() {
const fontSize = 0.05 * window.innerWidth - 10;
myCanvas.value.font = `${fontSize}px Adrip`;
myCanvas.value.textAlign = 'center';
const x = size.w / 2;
const lineHeight = fontSize;
const lines = texte.value.split('\n');
for (let i = 0; i < lines.length; i += 1) {
myCanvas.value.fillText(
lines[lines.length - i - 1],
x,
(size.h * 0.98) - (i * lineHeight),
);
}
}
function initCarte() {
const background = new Image();
background.src = '/img/fond.jpeg';
background.onload = function () {
rapport.value = background.naturalWidth / background.naturalHeight;
size.h = size.w / rapport.value;
try {
myCanvas.value.drawImage(background, 0, 0, size.w, size.h);
} catch (e) {
console.log(`ERREUR DE CHARGEMENT D'IMAGE: ${e}`);
}
drawText();
};
}
function handleResize() {
size.w = window.innerWidth * 0.8;
size.h = size.w / rapport.value;
initCarte();
}
window.addEventListener('resize', handleResize);
onMounted(() => {
const c = document.getElementById('myCanvas');
const ctx = c.getContext('2d');
myCanvas.value = ctx;
initCarte();
});
watch(texte, () => {
initCarte();
});
return {
myCanvas,
size,
texte,
};
},
});
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
#font-face {
font-family: 'Adrip';
src: local('Adrip'), url('/fonts/adrip1.ttf') format('truetype');
}
#myCanvas {
border: 1px solid grey;
}
</style>
Look at this line:
h: (window.innerWidth * 0.8) / 1.8083832335329342,
If I don't hardcode this and only put the canonical value window.innerWidth * 0.8, the image doesn't display, although the size.h = size.w / rapport.value; line executes correctly.
I really don't understand this behaviour, could somebody explain it to me?
Also, if anybody has a clue on how it would be possible to load the image once and for all so that I don't have to load it at every refresh, it would be better :)
Thanks in advance!
Your problem is that you change the size of the canvas after drawing the image, due to how templating magic works. If you put debugger behind drawText(); in the background onload function, you will see that it actually draws the image. However, in this same function, you set size.h. size is reactive, and is thus marked as "dirty". size is also used in the template, so the template is marked dirty. After the onload function is executed, Vue will rerender your template... and erase your image.
I think your best bet here is to use nextTick. You need to use it sparingly, but I think this is one of the instances where you have no choice but to wait for the DOM to settle. To do this, import nextTick from vue:
import { nextTick } from 'vue';
Then surround your drawImage try-catch block with that.
background.onload = function () {
rapport.value = background.naturalWidth / background.naturalHeight;
size.h = size.w / rapport.value;
nextTick(() => {
try {
myCanvas.value.drawImage(background, 0, 0, size.w, size.h);
} catch (e) {
console.log(`ERREUR DE CHARGEMENT D'IMAGE: ${e}`);
}
drawText();
});
};
As for your last question how to load the image once... the short answer is... you can't. Whenever the canvas changes, you need to redraw it. At least the image should be cached by the browser, so it just draws it from cache rather than doing another http request.

How to implement D3 for Vue.js

There are various implementations of D3 with React. One of the more interesting ones uses the react-faux-dom project. Advantages to this approach are that React knows about DOM elements created by D3 and the ability to create isomorphic charts.
Refer to the following:
http://oli.me.uk/2015/09/09/d3-within-react-the-right-way/
https://github.com/Olical/react-faux-dom
http://www.reactd3.org/
What would it take to implement D3 in Vue.js with the same benefits?
Is there a need to create something similar to react-faux-dom or does Vue already have something that can be used for this?
How does this approach make sense (or not) considering Vue’s architecture?
Since version 4, D3 is highly modularized and computational parts are well isolated in small librairies, such as d3-force. One approach I like is to let Vue.js handle DOM manipulation and events, and use d3.js for computations. Your visualization component is similar to your other components, and are easier to understand for someone familiar with Vue.js but not d3.js.
I created a codepen to show a force graph implementation :
HTML :
<div id="app">
<svg xmlns="http://www.w3.org/2000/svg" :width="width+'px'" :height="height+'px'" #mousemove="drag($event)" #mouseup="drop()" v-if="bounds.minX">
<line v-for="link in graph.links" :x1="coords[link.source.index].x" :y1="coords[link.source.index].y" :x2="coords[link.target.index].x" :y2="coords[link.target.index].y" stroke="black" stroke-width="2"/>
<circle v-for="(node, i) in graph.nodes" :cx="coords[i].x" :cy="coords[i].y" :r="20" :fill="colors[Math.ceil(Math.sqrt(node.index))]" stroke="white" stroke-width="1" #mousedown="currentMove = {x: $event.screenX, y: $event.screenY, node: node}"/>
</svg>
</div>
Javascript:
new Vue({
el: '#app',
data: {
graph: {
nodes: d3.range(100).map(i => ({ index: i, x: null, y: null })),
links: d3.range(99).map(i => ({ source: Math.floor(Math.sqrt(i)), target: i + 1 }))
},
width: Math.max(document.documentElement.clientWidth, window.innerWidth || 0),
height: Math.max(document.documentElement.clientHeight, window.innerHeight || 0) - 40,
padding: 20,
colors: ['#2196F3', '#E91E63', '#7E57C2', '#009688', '#00BCD4', '#EF6C00', '#4CAF50', '#FF9800', '#F44336', '#CDDC39', '#9C27B0'],
simulation: null,
currentMove: null
},
computed: {
bounds() {
return {
minX: Math.min(...this.graph.nodes.map(n => n.x)),
maxX: Math.max(...this.graph.nodes.map(n => n.x)),
minY: Math.min(...this.graph.nodes.map(n => n.y)),
maxY: Math.max(...this.graph.nodes.map(n => n.y))
}
},
coords() {
return this.graph.nodes.map(node => {
return {
x: this.padding + (node.x - this.bounds.minX) * (this.width - 2*this.padding) / (this.bounds.maxX - this.bounds.minX),
y: this.padding + (node.y - this.bounds.minY) * (this.height - 2*this.padding) / (this.bounds.maxY - this.bounds.minY)
}
})
}
},
created(){
this.simulation = d3.forceSimulation(this.graph.nodes)
.force('charge', d3.forceManyBody().strength(d => -100))
.force('link', d3.forceLink(this.graph.links))
.force('x', d3.forceX())
.force('y', d3.forceY())
},
methods: {
drag(e) {
if (this.currentMove) {
this.currentMove.node.fx = this.currentMove.node.x - (this.currentMove.x - e.screenX) * (this.bounds.maxX - this.bounds.minX) / (this.width - 2 * this.padding)
this.currentMove.node.fy = this.currentMove.node.y -(this.currentMove.y - e.screenY) * (this.bounds.maxY - this.bounds.minY) / (this.height - 2 * this.padding)
this.currentMove.x = e.screenX
this.currentMove.y = e.screenY
}
},
drop(){
delete this.currentMove.node.fx
delete this.currentMove.node.fy
this.currentMove = null
this.simulation.alpha(1)
this.simulation.restart()
}
}
})
The main drawback I see is if you have a large d3.js codebase you want to reuse in your Vue.js application as you will have to rewrite it. You will also find a lot of examples written in pure d3.js syntax and you will have to adapt them.

React animate elements in component

I want to animate some elements inside a component without using jQuery or another similar library. I've got the following code running:
export default React.createClass({
getInitialState() {
return {
women: {
total: this.props.women.upvotes + this.props.women.downvotes,
up_height: {
'height': 0
},
down_height: {
'height': 0
}
}
};
},
componentDidMount: function() {
this.setState({
women: {
up_height: {
'height': (this.props.women.upvotes / this.state.women.total) * 100 + '%'
},
down_height: {
'height': (this.props.women.downvotes / this.state.women.total) * 100 + '%'
}
}
})
},
render() {
return (
<li className="block-element">
<div className="upvotes">
<span className="women" style={this.state.women.up_height}>{this.props.women.upvotes}</span>
<span className="men">{this.props.men.upvotes}</span>
</div>
</li>
)
}
})
So on getInitialState the style height is set to 0. When de componentDidMount its set to the height it supposed to be. I did add a transition to the elements trough CSS, but nothing seem to happen.
I've tried adding a setTimeout around the setState in componentDidMount, but this broke the setState. Any heads up? How to make this work?
Another possibility is using requestAnimationFrame, as used in this (duplicate?) question.
If you do not mind using a library implementing this functionality specifically for React, react.animate (not to confuse with react-animate) does the thing. The current version (date: 9.4.16) on npm seems not to support CommonJS, but the version on github is more up to date. As an example:
npm install react react-dom http://github.com/pleasetrythisathome/react.animate/tarball/master
test.js :
React = require('react')
ReactDOM = require('react-dom')
require('#pleasetrythisathome/react.animate')
SomeComponent = React.createClass({
mixins: [React.Animate],
getInitialState: function () {return {bananas: 0}},
componentDidMount: function () {
this.animate({bananas: 100}, 1000, () => {console.log('Finish')})
},
render: function () {return React.DOM.span({}, this.state.bananas)},
})
ReactDOM.render(React.createElement(SomeComponent), document.getElementById('apple'))
then e.g. with browserify
browserify test.js > apple.js
test.html :
<div id="apple"></div>
<script src="apple.js"></script>
JSFiddle:
https://jsfiddle.net/jo5tp5mt/

React apply style to element using a wrapper

I'm trying to achieve the following:
<FadeInAnimation>
<SomeReactElement />
</FadeInAnimation>
The react element could be:
SomeReactElement.render = ()=>{
return <div>hi</div>
}
The FadeInAnimation just interpolates a style object and passes it to its child via React.cloneElement(this.props.children ...).
The problem is that the SomeReactElement cannot process a style prop properly (since it needs to know to which HTML tag element (such as div) to apply it onto).
I could just pass the {...props} to the div but then in more complex hierarchies such as
<ReactElement1>
<ReactElement2>
<SomeReactElement />
</ReactElement2>
</ReactElement1>
This becomes cumbersome.
What I'd really like is a convenience method that I could use in the FadeInAnimation component that could find the outer most React element which is of an HTML tag type.
I can't react it via this.props.children because the div is not SomeReactElement's child.
I'm aware that I could wrap the element within a or but it breaks the design in many cases.
I think you are over-complicating a simpler problem :)
If you want to implement a FadeAnimation component, it could simply wrap all it's children and animate that wrapper. Voilà!
Here's a pen with a complete example: http://codepen.io/gadr90/pen/Gpwdbq?editors=011
Basically:
const ReactCSSTransitionGroup = React.addons.CSSTransitionGroup
const FadeAnimation = React.createClass({
render() {
return (
<ReactCSSTransitionGroup
transitionName="example"
transitionEnterTimeout={500}
transitionLeaveTimeout={500}>
{this.props.show &&
<div key="content">
{this.props.children}
</div>
}
</ReactCSSTransitionGroup>
)
}
})
const App = React.createClass({
getInitialState() {
return { show: false }
},
toggle() {
this.setState({ show: !this.state.show })
},
render() {
return (
<div>
<button onClick={this.toggle}>Toggle</button>
<FadeAnimation show={this.state.show}>
<div key="content">Content</div>
</FadeAnimation>
</div>
)
}
});
React.render(<App />, document.getElementById('content'))
And for CSS:
.example-enter {
opacity: 0.01;
}
.example-enter.example-enter-active {
opacity: 1;
transition: opacity 500ms ease-in;
}
.example-leave {
opacity: 1;
}
.example-leave.example-leave-active {
opacity: 0.01;
transition: opacity 500ms ease-in;
}

React component with Dagre-D3 not drawing correctly

I converted this Dagre-D3 demo to a React component. The code is below.
import React from 'react'
import d3 from 'd3'
import dagreD3 from 'dagre-d3'
export default class D3Chart extends React.Component {
constructor () {
super();
}
componentDidMount() {
// Create the input graph
var g = new dagreD3.graphlib.Graph()
.setGraph({})
.setDefaultEdgeLabel(function() { return {}; });
// Here we"re setting nodeclass, which is used by our custom drawNodes function
// below.
g.setNode(0, { label: "TOP", class: "type-TOP" });
g.setNode(1, { label: "S", class: "type-S" });
g.setNode(2, { label: "NP", class: "type-NP" });
g.setNode(3, { label: "DT", class: "type-DT" });
g.setNode(4, { label: "This", class: "type-TK" });
g.setNode(5, { label: "VP", class: "type-VP" });
g.setNode(6, { label: "VBZ", class: "type-VBZ" });
g.setNode(7, { label: "is", class: "type-TK" });
g.setNode(8, { label: "NP", class: "type-NP" });
g.setNode(9, { label: "DT", class: "type-DT" });
g.setNode(10, { label: "an", class: "type-TK" });
g.setNode(11, { label: "NN", class: "type-NN" });
g.setNode(12, { label: "example", class: "type-TK" });
g.setNode(13, { label: ".", class: "type-." });
g.setNode(14, { label: "sentence", class: "type-TK" });
g.nodes().forEach(function(v) {
var node = g.node(v);
// Round the corners of the nodes
node.rx = node.ry = 5;
});
// Set up edges, no special attributes.
g.setEdge(3, 4);
g.setEdge(2, 3);
g.setEdge(1, 2);
g.setEdge(6, 7);
g.setEdge(5, 6);
g.setEdge(9, 10);
g.setEdge(8, 9);
g.setEdge(11,12);
g.setEdge(8, 11);
g.setEdge(5, 8);
g.setEdge(1, 5);
g.setEdge(13,14);
g.setEdge(1, 13);
g.setEdge(0, 1)
// Create the renderer
var render = new dagreD3.render();
// Set up an SVG group so that we can translate the final graph.
var svg = d3.select(React.findDOMNode(this.refs.nodeTree));
var svgGroup = d3.select(React.findDOMNode(this.refs.nodeTreeGroup));
// Run the renderer. This is what draws the final graph.
render(d3.select(React.findDOMNode(this.refs.nodeTreeGroup)), g);
// Center the graph
var xCenterOffset = (svg.attr("width") - g.graph().width) / 2;
svgGroup.attr("transform", "translate(" + xCenterOffset + ", 20)");
svg.attr("height", g.graph().height + 40);
}
render() {
return (<svg id="nodeTree" ref="nodeTree" width="960" height="600"><g ref="nodeTreeGroup"/></svg>
)
};
}
The problem is that the rendering of the nodes are mis-aligned and their sizes too.
This is how it looks like. How it should like is here.
UPDATE:
This is how the first node looks like:
What now:
<g class="node type-TOP" transform="translate(100,0)" style="opacity: 1;"><rect rx="5" ry="5" x="-10" y="-10" width="20" height="20"></rect><g class="label" transform="translate(0,0)"><g transform="translate(0,0)"><text><tspan xml:space="preserve" dy="1em" x="1">TOP</tspan></text></g></g></g>
What should be:
<g class="node type-TOP" transform="translate(211.25,18)" style="opacity: 1;"><rect rx="5" ry="5" x="-24.5" y="-18" width="49" height="36"></rect><g class="label" transform="translate(0,0)"><g transform="translate(-14.5,-8)"><text><tspan xml:space="preserve" dy="1em" x="1">TOP</tspan></text></g></g></g>
The width and height are not calculated correctly. The width should be 49 but it is only 20.
Try to set the height and width inside the componentDidMount just after you caught the svg with findDOMNode.
Or try to put the height and width this way.
style={{height:'900', width:'300'}}
Let me know if it works
I've figured out a workaround for this. I am having the exact same issue as you, and through pure trial and error I discovered that if you append the graph to the body of the page, it works fine. The text and nodes line up and the node sizes change appropriately to the text. This isn't ideal, but is a workaround..
(I don't know react syntax, so bear with me)
Instead of having a svg node pre created like this (don't do this):
var svg = d3.select(React.findDOMNode(this.refs.nodeTree));
Build one on the fly and append it to the body element of your whole HTML doc (do this):
var svg = d3.select(React.findDOMNode('body'))
.append('svg')
.attr('width', 500)
.attr('height', 500);
I'm not sure why this is, possibly the reason you explained in the comments of François Richard's answer.

Resources