Nowadays there’re lots of technologies in a Front-end and most of them have easily understandable tutorials. So starting with them is not a problem. But there’s always a problem when you try to use some of them together. In this article, we’re going to talk about a usage of Angular or Vue.js together with D3.js, and find out why we might have problems with the integration.
When you integrate different technologies a very first question that you should resolve is defining of responsibilities. Each technology should do exactly the thing that it was brought for. D3 and MV frameworks integration is not an exception, but it has its own pitfalls.
From the chart above we can understand clearly enough that both of our technologies get some data and represent it in the form of view. So basically they resolve particular the same problem in our case, and defining of responsibility zone for each can be a bit tricky.
Let’s look behind and remember what is the framework and how it’s different from a library. The main difference here is that the framework dictates you the structure of a code, how components should interact, be defined and etc.
While library just brings some module to be used in your project. So now we can do sort of flattening and decide who is in charge?
What conclusions can we do after such flattening?
- Framework is responsible for the entire architecture of application;
- Framework is responsible for all the data flows of application (gets data and provides it to components);
- D3.js is responsible only for SVG rendering with no ways to interact with data source directly as well as change it;
In shorter words: D3.js logic is a part of a common component, it takes props with the data, does rendering and sends events back to main components/storages then there’s a need to mutate a data.
Angular note
For a long time in AngularJS it was a good practice to push everything that’s not connected with business logic into the services/factories, in the case of D3 rendering it can be an anti-pattern for you because of following reasons:
- Logic of D3 handlers interaction with framework related stuff won’t be transparent enough being pushed into the service (because as you remember from time to timeyou
want to send some commands into the other components); - Creation of generic service that will do the full diagram rendering is a generally bad idea (just remember I of SOLID principles);
And just remember that the main purpose of services in Angular is to share logic between components. In our case it means one short rule:
Otherwise stay in component.
The question of integration is not a really outstanding thing, you just need to include rendering logic based on d3 into your component like this:
private init() { this.svg = d3.select(this.$el) .select('svg'); if (this.chart) { this.chart.remove(); } this.chart = this.svg .append('g') .attr('transform', `translate(${constants.chart.padding}, ${constants.chart.padding})`); this.generateAxis(); this.generateData(); }
And call this kind of method whenever you need a chart render (generateAxis and generateData are obviously methods that include corresponding logic). But this whenever can be a serious problem after all and to see where exactly we can meet a pitfall let’s examine the component structure of our test application. Our application is a simple Twitch streamer game popularity chart (shows a correlation between average online viewers of stream and game) with an ability to select exact game and see detailed info.
As you can see our Chart component needs to communicate with Main component in 2 different ways:
- Gets chart data prop and re-renders chart content whenever prop changed;
- Sends selected chart entry info back to Main;
While D3 is responsible for all the stuff like data-based axis drawing, scaling it to fit the window, positioning of all the required labels, line points calculation and etc. But if we use it for full rendering we need to track input props mutations fully by ourself, the framework has nothing to do with props anymore. But our todays guests have ways to workaround this situation.
In the case of Vue.js it’s really simple and straightforward solution – use watcher ( a function that will be called wherever property that’s bound to it is mutated):
@Watch('data') private onDataChange() { this.data && this.init(); }
In the case of Angular we have 2 possible options:
Simple one – use setter for @Input change handling.
@Input() public set data(value: ChartEntry[]) { if (this._data && this._data !== value) { this.init(); } this._data = value; }
Hard one – use onChanges lifecycle hook, that takes every single change in all props of the component. This way is more preferable for the cases when a diagram is really large and you want to do partial re-rendering via tracking of exact entry change for example.
Another thing that we need to handle is child-to-parent communication from d3.js handler, again Vue.js and Angular can do it without any problems.
In the Vue case just use $emit on the parent level where it will be handled:
this.chart.append('circle') .attr('cx', xScale(index + 1)) .attr('cy', yScale(this.data[index].averageViewers)) .attr('r', constants.chart.pointR) .on('click', () => { this.$parent.$emit('change-selected-index', index); })
In the Angular case, we need to specify Output of component…
@Output() public selectEntry = new EventEmitter<number>();
… and use it to emit value back to the parent:
this.chart.append('circle') .attr('cx', xScale(index + 1)) .attr('cy', yScale(this.data[index].averageViewers)) .attr('r', this.constants.chart.pointR) .on('click', () => { this.selectEntry.emit(index); })
The code can be found in this repository and of course, you’re welcome to ask any questions in article comments!
Instead of PS
Always remember that D3.js is an instrument as well as any other and there’s no silver bullet in the programming. So if you see that your SVG is going to have 1-2 primitive elements inside and that’s it, then there’s no need to include D3. In such cases, you can easily render everything using framework abilities. You can find an appropriate example of such rendering inside Vue-primitive-svg folder of example repository.
We are looking forward to meeting you on our website soshace.com