23
Dec
2014
0 Comments

D3 Advanced Brush Styling

Rebroadcasting: I originally wrote this article on May 30, 2013 for the Emcien Engineering blog.



I wanted to make a D3 brush that looked a little nicer than a colored rectangle overlaying a chart. This is what I came up with:


Lets walk through building this, step by step.


Part 1 The standard brush

The first thing we are going to do is create the initial chart and brush:

var data = [{
    date: new Date("Jan 01, 2013"),
    data: 12
},{
    date: new Date("Jan 02, 2013"),
    data: 17
// ...
data.js
function run() {
    var w = 750;
    var h = 100;
 
    var x = d3.time.scale()
        .range( [0, w] )
        .domain( [data[0].date, data[data.length-1].date] );
 
    var y = d3.scale.linear()
        .range( [h, 0] )
        .domain( [0, 20] );
 
    var svg = d3.select("body").append("svg");
    var focus = svg.append("g");
 
    var line = d3.svg.line()
        .interpolate("basis")
        .x(function(d){ return x(d.date); })
        .y(function(d){ return y(d.data); });
 
    var area = d3.svg.area()
        .interpolate("basis")
        .x(function(d){ return x(d.date); })
        .y1(function(d){ return y(d.data); })
        .y0(function(d){ return y(0); });
 
    var brush = d3.svg.brush().x(x);
 
    focus.append("path")
        .attr("class", "area")
        .style({"fill": "#ccc"})
        .datum(data)
        .attr("d", area);
 
    focus.append("path")
        .attr("class", "line")
        .style({
            "fill": "none",
            "stroke": "#000",
            "stroke-width": "2"
        })
        .datum(data)
        .attr("d", line);
 
    focus.append("g")
        .attr("class","x brush")
        .call(brush.extent([data[7].date,data[11].date]))
        .selectAll("rect")
        .attr("height", h)
        .style({
            "fill": "#69f",
            "fill-opacity": "0.3"
        });
}
main.js

Nothing new here, this just creates a chart with a standard brush:





Part 2 The mask

Now to get my effect I needed to create a mask. This little class I wrote does the trick pretty nicely:

var SVGMask = (function() {
 
    function SVGMask(focus) {
        this.focus = focus;
        this.mask  = this.focus.append("g").attr("class", "mask");
        this.left  = this.mask.append("polygon");
        this.right = this.mask.append("polygon");
        this._x = null;
        this._y = null;
    }
 
    SVGMask.prototype.style = function(prop, val) {
        this.left.style(prop, val);
        this.right.style(prop, val);
        return this;
    }
 
    SVGMask.prototype.x = function(f) {
        if (f == null) {
            return this._x;
        }
        this._x = f;
        return this;
    };
 
    SVGMask.prototype.y = function(f) {
        if (f == null) {
            return this._y;
        }
        this._y = f;
        return this;
    };
 
    SVGMask.prototype.redraw = function() {
        var lp, maxX, maxY, minX, minY, rp, xDomain, yDomain;
 
        yDomain = this._y.domain();
        minY = yDomain[0];
        maxY = yDomain[1];
 
        xDomain = this._x.domain();
        minX = xDomain[0];
        maxX = xDomain[1];
 
        lp = {
            l: this._x(minX),
            t: this._y(minY),
            r: this._x(this.from),
            b: this._y(maxY)
        };
 
        rp = {
            l: this._x(this.to),
            t: this._y(minY),
            r: this._x(maxX),
            b: this._y(maxY)
        };
 
        this.left.attr("points", "" + lp.l + "," + lp.t + "  " + lp.r + "," + lp.t + "  " + lp.r + "," + lp.b + "  " + lp.l + "," + lp.b);
        this.right.attr("points", "" + rp.l + "," + rp.t + "  " + rp.r + "," + rp.t + "  " + rp.r + "," + rp.b + "  " + rp.l + "," + rp.b);
 
        return this;
    };
 
    SVGMask.prototype.reveal = function(extent) {
        this.from = extent[0];
        this.to = extent[1];
        this.redraw();
        return this;
    };
 
    return SVGMask;
 
})();
mask.js

The way this mask works is, you give it something to mask; then specify the domain using the x() and y() methods. You can then reveal a portion by calling the reveal() method and passing an extent. This is very similar to calling extent() on the brush. Only instead of specifying the area that the brush should cover, it specifies an area not to cover.

Now just need to update main.js to make use of this mask:

var mask = new SVGMask(focus)
    .x(x)
    .y(y)
    .style("fill","red")
    .reveal([data[8].date,data[14].date]);
This snippet needs to be put between where the area and line are added to the screen. This ensures that the mask is in the proper position in the stack to cover what we want and only what we want. The result is this:



Notice how the gray area is being masked while the black line is not. I only temporarily set the mask to red to make it clear what is happening.

Now we just need to make sure that the portion of the area being revealed by the mask lines up to the extent of the brush. So we update main.js with this snippet:
brush.on("brush", function(){
    mask.reveal(brush.extent());
});
Now the mask follows the brush quite nicely:



Try moving the brush around to see how the mask stays aligned to it.

And if we get rid of that red fill on our mask we have this:





Part 3 Final touches

Now to make this effect complete we need to add our left and right handles:
var leftHandle = focus.append("image")
    .attr("width", 15)
    .attr("height", 100)
    .attr("x", x(data[7].date)-5)
    .attr("xlink:href", 'left-handle.png');
 
var rightHandle = focus.append("image")
    .attr("width", 15)
    .attr("height", 100)
    .attr("x", x(data[11].date)-7)
    .attr("xlink:href", 'right-handle.png');
And we also need to update our brush event to keep them in sync:
brush.on("brush",function(){
    var ext = brush.extent();
    mask.reveal(ext);
    leftHandle.attr("x", x(ext[0])-5);
    rightHandle.attr("x", x(ext[1])-7);
});
Finally we will want to hide the actual brush itself:
focus.append("g")
    .attr("class", "x brush")
    .call(brush.extent([data[7].date,data[11].date]))
    .selectAll("rect")
    .attr("height", h)
    .style({"fill": "none"});
And now we have our final result:



Thats it. We now have a much better looking brush. I hope you enjoyed this tutorial, you can get the final source code here.
31
Aug
2013
2 Comments

Tips For Creating Sublime Text Color Schemes

So I recently created my first Sublime Text color scheme, Deep Blue See. In doing so I discovered what a pain it can be. But it doesn't have to be a pain, in fact, with these 2 tips the process can be a lot smoother.

Tip 1 Know your scopes:


Sublime text color schemes work by defining colors for scopes. A syntax definition matches the different parts of the file's text (e.g. functions, classes, keywords, etc.) and maps them to a named scope. Then the color scheme specifies what colors to use for what scopes.

The hard part comes when you see a particular piece of syntax you want to style a specific way, but you do not know what scope it is. I did a lot of guess work until I discovered the ScopeHunter plugin.

The ScopeHunter plugin allows you to select some text and it tells you what scope it matches. This removes the guess work and allows you to quickly color the pieces you want to.

Tip 2 Stop fiddling with XML and JSON files:


Sublime Text color schemes are defined with Property lists; which are just cumbersome XML documents that are tedious to work with. So whats the Sublime Community's answer to this? JSON, which is itself cumbersome and tedious to work with, albeit slightly less so then the property lists.

Well fuck that. Use Aroma instead. Aroma lets you compile a CoffeeScript object into a Property List. Now I know there is a great many people that dislike CoffeeScript (I happen to love it!) or simply just don't care to learn it. Well you don't have to. In fact, you don't even need to think of it as CoffeeScript, think of it more like Sass for Sublime Text color schemes.

You aren't writing logic (unless you want to), just key-value pairs. Which ends up looking just like Sass.
Here is a snippet from DeepBlueSee:

CoffeeScript Object:
name:         "Ruby: Constant"
scope:        "constant.language.ruby, constant.numeric.ruby"
settings:
  foreground: $PURPLE
Compared to Sass:
float:        left
color:        $RED
.inner:
  background: $PURPLE
You also get to use variables just like in Sass. Woo hoo!


Aroma expects the objects to be defined in .aroma.coffee files that export the objects as node modules (Aroma really is just a node module itself!) It's actually really easy, just export the object like so:
module.exports =
  foo: "Static"
  baz: "bar"
This format allows you to specify variables to be reused like so:
foo = "Dynamic!"
red = "#d42e64"
module.exports =
  foo: foo
  baz: red

Aroma defaults to compiling to .plist files, but Sublime uses the .tmTheme extension. You can specify this like so:
$ aroma -e ".tmTheme"
You can even have aroma watch for changes and recompile as you work with the --watch flag:
$ aroma -we ".tmTheme"

Well there you have it. I hope this inspires some really great color schemes! If you like aroma, drop me line in the comments or on twitter.
29
May
2013
3 Comments

MaxPane

So I have released my second Sublime Text plugin. I always liked the split panes in Sublime Text but sometimes the one I was currently working in, I would want to maximize so I could really focus. The problem was, all I could do was reset it to a single pane view, so when I switched back to a dual column setup again all my files were still in the left pane.

Lets say I was working in File B in the following split pane:

If I switched to a single pane, then back to 2 columns it would be like:


This may seem like a minor annoyance to have to reposition the pane divider to make the right side slightly larger and put File B back over to the right; but do it again and again day after day and this minor annoyance begins to become very annoying.

For a long time I actually just dragged the pane I wanted maximized over like so:


But this eventually got old too and I decided it was time to tackle my next plugin.

The part of the API I needed to accomplish this is undocumented, but luckily there is another Sublime package that deals very closely to the same problem domain. I was able to read the source code and tinker enough to figure out how to solve the problem.

The plugin lets you actually fully maximize a pane, while keeping a snap shot of the last layout so you can easily switch back. just type cmd+shift+enter to toggle back and forth. I used that keyboard shortcut because it does the same thing in iTerm 2.

I also added keyboard shortcuts to jump between maximized panes. To learn more check out the readme.