Skip to content

Instantly share code, notes, and snippets.

@mayo
Last active August 29, 2015 14:01

Revisions

  1. mayo revised this gist Jul 28, 2014. 1 changed file with 3 additions and 1 deletion.
    4 changes: 3 additions & 1 deletion README.md
    Original file line number Diff line number Diff line change
    @@ -1 +1,3 @@
    Expanding on mbostock's [weekday.js](https://gist.github.com/mbostock/5827353) and my [Weekdays](https://gist.github.com/mayo/c0c1ca7f5bbb30cc3ef3) gists by adding adaptive tick mark format and wrapping it all up in a dayselect scale (`d3.scale.dayselect`).
    Expanding on mbostock's [weekday.js](https://gist.github.com/mbostock/5827353) and my [Weekdays](https://gist.github.com/mayo/c0c1ca7f5bbb30cc3ef3) gists by adding adaptive tick mark format and wrapping it all up in a dayselect scale (`d3.scale.dayselect`).

    View at [bl.ocks.org](http://bl.ocks.org/mayo/e27554b34bff1f177c05);
  2. mayo revised this gist Jul 28, 2014. 1 changed file with 4 additions and 3 deletions.
    7 changes: 4 additions & 3 deletions weekday.js
    Original file line number Diff line number Diff line change
    @@ -43,7 +43,8 @@
    if (c != null) {
    return c;
    }


    var lookupWeekdays = weekdays;
    var year = 1970,
    yearWeekdays;

    @@ -60,8 +61,8 @@

    date = new Date(year, 0, (weekdays / 5 | 0) * 7 + days + 1);

    cache[date] = weekdays;
    cache[weekdays] = date;
    cache[date] = lookupWeekdays;
    cache[lookupWeekdays] = date;

    return date;
    };
  3. mayo revised this gist Jul 22, 2014. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions index.html
    Original file line number Diff line number Diff line change
    @@ -52,8 +52,8 @@

    var xAxis = d3.svg.axis()
    .scale(x)
    .orient("below")
    .tickFormat(function (d) { return dateFormat(weekday.invert(d)); });
    .orient("below");
    // .tickFormat(function (d) { return dateFormat(weekday.invert(d)); });

    var yAxis = d3.svg.axis()
    .scale(y)
  4. mayo revised this gist Jul 22, 2014. 1 changed file with 4 additions and 4 deletions.
    8 changes: 4 additions & 4 deletions dayselect.js
    Original file line number Diff line number Diff line change
    @@ -129,10 +129,10 @@ d3.scale.dayselect = d3.scale.dayselect = function(mapFunction) {
    [d3.time.hour, 12],
    [d3.time.day, 1],
    [d3.time.day, 2],
    [d3.time.week, 1],
    [d3.time.month, 1],
    [d3.time.month, 3],
    [d3.time.year, 1]
    [d3.time.day, 5], //.week, 1
    [d3.time.day, 22], //.month, 1
    [d3.time.day, 66], //.month, 3
    [d3.time.day, 261] //.year, 1
    ];

    function dayselect_time_formatMulti(formats) {
  5. mayo revised this gist Jul 22, 2014. 1 changed file with 16 additions and 1 deletion.
    17 changes: 16 additions & 1 deletion weekday.js
    Original file line number Diff line number Diff line change
    @@ -14,7 +14,22 @@
    while (--year >= 1970) weekdays += weekdaysInYear(year);

    cache[date] = weekdays;
    cache[weekdays] = date;

    //if we're looking up a weekend day, make sure we cache the correct weekday
    if (cache[weekdays] == null) {

    newDate = new Date(date);
    offset = newDate.getDay() == 0 ? -2 : newDate.getDay() == 6 ? -1 : 0;

    if (offset > 0) {
    date.setDate(date.getDate() + offset);

    //cache the new date as well
    cache[newDate] = weekdays;
    }

    cache[weekdays] = newDate;
    }

    return weekdays;
    }
  6. mayo revised this gist Jul 22, 2014. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion README.md
    Original file line number Diff line number Diff line change
    @@ -1 +1 @@
    Expanding on mbostock's [weekday.js](https://gist.github.com/mbostock/5827353) and my [Weekdays](https://gist.github.com/mayo/c0c1ca7f5bbb30cc3ef3) gists by adding adaptive tick mark format and wrapping it all up in a dayselect scale (1d3.scale.dayselect1).
    Expanding on mbostock's [weekday.js](https://gist.github.com/mbostock/5827353) and my [Weekdays](https://gist.github.com/mayo/c0c1ca7f5bbb30cc3ef3) gists by adding adaptive tick mark format and wrapping it all up in a dayselect scale (`d3.scale.dayselect`).
  7. mayo revised this gist May 13, 2014. 2 changed files with 2 additions and 4 deletions.
    2 changes: 1 addition & 1 deletion README.md
    Original file line number Diff line number Diff line change
    @@ -1 +1 @@
    Expanding on mbostock's [weekday.js](https://gist.github.com/mbostock/5827353) and my [Weekdays](https://gist.github.com/mayo/c0c1ca7f5bbb30cc3ef3) gists by adding adaptive tick mark format and wrapping it all up in a `dayselect` scale (d3.scale.dayselect).
    Expanding on mbostock's [weekday.js](https://gist.github.com/mbostock/5827353) and my [Weekdays](https://gist.github.com/mayo/c0c1ca7f5bbb30cc3ef3) gists by adding adaptive tick mark format and wrapping it all up in a dayselect scale (1d3.scale.dayselect1).
    4 changes: 1 addition & 3 deletions dayselect.js
    Original file line number Diff line number Diff line change
    @@ -8,9 +8,7 @@
    // This scale is currently tailored to weekday scale, as the values in
    // dayselect_time_scaleSteps reflect 5 day weeks. It should be possible to
    // calculate these values based on the map function, rather than hardcoding
    // them into the function. The scaleSteps values are exposed through
    // .scaleSteps property, in case they need to be adjusted. Their fundamental
    // values should not change, though.
    // them into the function.
    //
    // In theory, it should be possible to use any function that does similar
    // mapping, for eg. business hours. Let's call it weekhours and each hour of a
  8. mayo revised this gist May 13, 2014. 4 changed files with 372 additions and 0 deletions.
    6 changes: 6 additions & 0 deletions data.csv
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,6 @@
    date,value
    01/5/2014,1
    02/5/2014,2
    05/5/2014,1
    06/5/2014,2
    08/5/2014,2
    180 changes: 180 additions & 0 deletions dayselect.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,180 @@
    // Expecting a function that can map between non-uniform day scale (weekdays,
    // for eg.) and a linear scale. The map function needs to return linear values
    // as output, given a input Date(), respond to .invert() given a value from
    // linear scale and return a corresponding Date() object, and a .factor
    // property containing a multplier to convert the linear values to
    // miliseconds.
    //
    // This scale is currently tailored to weekday scale, as the values in
    // dayselect_time_scaleSteps reflect 5 day weeks. It should be possible to
    // calculate these values based on the map function, rather than hardcoding
    // them into the function. The scaleSteps values are exposed through
    // .scaleSteps property, in case they need to be adjusted. Their fundamental
    // values should not change, though.
    //
    // In theory, it should be possible to use any function that does similar
    // mapping, for eg. business hours. Let's call it weekhours and each hour of a
    // business week would be mapped onto a uniform scale just like weekdays.
    // Performance of this kind of map may be an issue though.

    d3.scale.dayselect = d3.scale.dayselect = function(mapFunction) {

    function dayselect_scale(linear, methods, format, mapFunction) {

    function scale(x) {
    return linear(x);
    }

    function tickMethod(extent, count) {
    var span = extent[1] - extent[0];
    //var target = span / count;
    var target = span * mapFunction.factor / count;
    var i = d3.bisect(dayselect_time_scaleSteps, target);
    /* changing 31536e6 to 22550.4e6, to factor for shorter years */
    return i == dayselect_time_scaleSteps.length ? [dayselect_time_scaleLocalMethods.year, dayselect_scale_linearTickRange(extent.map(function(d) { return d / 22550.4e6; }), count)[2]]
    : !i ? [dayselect_time_scaleMilliseconds, dayselect_scale_linearTickRange(extent, count)[2]]
    : dayselect_time_scaleLocalMethods[target / dayselect_time_scaleSteps[i - 1] < dayselect_time_scaleSteps[i] / target ? i - 1 : i];
    }

    scale.ticks = function(interval, skip) {
    var extent = dayselect_scaleExtent(x.domain());
    var method = interval == null ? tickMethod(extent, 10)
    : typeof interval === "number" ? tickMethod(extent, interval)
    : !interval.range && [{range: interval}, skip]; // assume deprecated range function

    if (method) interval = method[0], skip = method[1];

    //return
    out = interval.range(mapFunction.invert(extent[0]), mapFunction.invert(+extent[1] + 1), skip < 1 ? 1 : skip); // inclusive upper bound

    //convert to weekdays
    return out.map(function(e) { return mapFunction(e); });
    }

    scale.tickFormat = function() {
    return format;
    };

    scale.copy = function() {
    return dayselect_scale(linear.copy(), methods, format, mapFunction);
    };

    return d3.rebind(scale, linear, "nice", "domain", "invert", "range", "rangeRound", "interpolate", "clamp");
    }

    /* clean copy from d3, becase we're crossing namespaces */
    function dayselect_scale_linearTickRange(domain, m) {
    if (m == null) m = 10;

    var extent = dayselect_scaleExtent(domain),
    span = extent[1] - extent[0],
    step = Math.pow(10, Math.floor(Math.log(span / m) / Math.LN10)),
    err = m / span * step;

    // Filter ticks to get closer to the desired count.
    if (err <= .15) step *= 10;
    else if (err <= .35) step *= 5;
    else if (err <= .75) step *= 2;

    // Round start and stop values to step interval.
    extent[0] = Math.ceil(extent[0] / step) * step;
    extent[1] = Math.floor(extent[1] / step) * step + step * .5; // inclusive
    extent[2] = step;
    return extent;
    }

    /* clean copy from d3, becase we're crossing namespaces */
    function dayselect_scaleExtent(domain) {
    var start = domain[0], stop = domain[domain.length - 1];

    return start < stop ? [start, stop] : [stop, start];
    }

    /* clean copy from d3, becase we're crossing namespaces */
    function dayselect_time_scaleDate(t) {
    return new Date(mapFunction.invert(t));
    }

    var dayselect_time_scaleSteps = [
    1e3, // 1-second
    5e3, // 5-second
    15e3, // 15-second
    3e4, // 30-second
    6e4, // 1-minute
    3e5, // 5-minute
    9e5, // 15-minute
    18e5, // 30-minute
    36e5, // 1-hour
    108e5, // 3-hour
    216e5, // 6-hour
    432e5, // 12-hour
    864e5, // 1-day
    1728e5, // 2-day
    4320e5, // 1-week // 5 days. original value 6048e5 = 7 days
    1900.8e6, // 1-month // 22 days is 21 better?. orignal value 2592e6 = 30 days
    5702.4e6, // 3-month // 66 days. is 63 better?. orignal value 7776e6 = 90 days
    22550.4e6 // 1-year //261 days. is 260 better?. original value 31536e6 = 365 days
    ];

    var dayselect_time_scaleLocalMethods = [
    [d3.time.second, 1],
    [d3.time.second, 5],
    [d3.time.second, 15],
    [d3.time.second, 30],
    [d3.time.minute, 1],
    [d3.time.minute, 5],
    [d3.time.minute, 15],
    [d3.time.minute, 30],
    [d3.time.hour, 1],
    [d3.time.hour, 3],
    [d3.time.hour, 6],
    [d3.time.hour, 12],
    [d3.time.day, 1],
    [d3.time.day, 2],
    [d3.time.week, 1],
    [d3.time.month, 1],
    [d3.time.month, 3],
    [d3.time.year, 1]
    ];

    function dayselect_time_formatMulti(formats) {
    var n = formats.length, i = -1;

    while (++i < n) {
    formats[i][0] = d3.time.format(formats[i][0]);
    }

    return function(date) {
    date = mapFunction.invert(date);

    var i = 0, f = formats[i];

    while (!f[1](date)) {
    f = formats[++i];
    }

    return f[0](date);
    };
    }

    var dayselect_time_scaleLocalFormat = dayselect_time_formatMulti(([
    [".%L", function(d) { return d.getMilliseconds(); }],
    [":%S", function(d) { return d.getSeconds(); }],
    ["%I:%M", function(d) { return d.getMinutes(); }],
    ["%I %p", function(d) { return d.getHours(); }],
    ["%a %d", function(d) { return d.getDay() && d.getDate() != 1; }],
    ["%b %d", function(d) { return d.getDate() != 1; }],
    ["%B", function(d) { return d.getMonth(); }],
    ["%Y", function() { return true; }]
    ]));

    var dayselect_time_scaleMilliseconds = {
    range: function(start, stop, step) { return d3.range(Math.ceil(start / step) * step, +stop, step).map(dayselect_time_scaleDate); },
    floor: d3.identity,
    ceil: d3.identity
    };

    dayselect_time_scaleLocalMethods.year = d3.time.year;

    return dayselect_scale(d3.scale.linear(), dayselect_time_scaleLocalMethods, dayselect_time_scaleLocalFormat, mapFunction);
    };
    114 changes: 114 additions & 0 deletions index.html
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,114 @@
    <!DOCTYPE html>
    <html>
    <head>
    <meta charset="utf-8">

    <script src="http://d3js.org/d3.v3.min.js"></script>
    <script src="weekday.js"></script>
    <script src="dayselect.js"></script>
    <title>Scale test</title>
    <style>
    body {
    font-family: 'helvetica neue';
    font-size: .8em;
    }
    .line {
    fill: none;
    stroke: black;
    stroke-width: 1px;
    }

    .axis line,
    .axis path {
    stroke-width: 1px;
    stroke: black;
    fill: none;
    }

    .dots circle {
    fill: #555;
    fill-opacity: .5;
    }
    </style>
    </head>
    <body>
    <div id="chart"></div>
    <script>
    var margin = {top: 20, right: 50, bottom: 50, left: 20},
    width = 960 - margin.left - margin.right,
    height = 502 - margin.top - margin.bottom;

    var parseDate = d3.time.format("%d/%m/%Y").parse;

    var dayCount = 0;

    var x = d3.scale.dayselect(weekday)
    .range([0, width - margin.right]);

    var y = d3.scale.linear()
    .range([0, height - margin.left]);

    var dateFormat = d3.time.format('%a %b %d');

    var xAxis = d3.svg.axis()
    .scale(x)
    .orient("below")
    .tickFormat(function (d) { return dateFormat(weekday.invert(d)); });

    var yAxis = d3.svg.axis()
    .scale(y)
    .orient("right");

    var line = d3.svg.line()
    .x(function(d) { return x(d.weekday); })
    .y(function(d) { return y(d.value); });

    var svg = d3.select("#chart").append("svg")
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom);

    d3.csv("data.csv", type, function(error, data) {
    x.domain(d3.extent(data, function(d) { return d.weekday; }));
    y.domain(d3.extent(data, function(d) { return parseFloat(d.value); }))

    svg.append("path")
    .datum(data)
    .attr("class", "line")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
    .attr("d", line);

    svg.append("g")
    .attr("class", "x axis")
    .attr("transform", "translate(" + margin.left + "," + (margin.top + height + 20) + ")")
    .call(xAxis)
    .selectAll("text")
    .attr("dy", ".35em");

    svg.append("g")
    .attr("class", "y axis")
    .attr("transform", "translate(" + (width + 20) + "," + margin.top + ")")
    .call(yAxis)
    .selectAll("text")
    .attr("dy", ".35em");

    svg.append("g")
    .attr("class", "dots")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")")
    .selectAll("circle")
    .data(data)
    .enter()
    .append("circle")
    .attr("r", 5)
    .attr("cx", function(d) { return x(d.weekday); })
    .attr("cy", function(d) { return y(d.value); });;
    });

    function type(d) {
    d.date = parseDate(d.date);
    d.weekday = weekday(d.date);

    return d
    }
    </script>
    </body>
    </html>
    72 changes: 72 additions & 0 deletions weekday.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,72 @@
    weekday = (function() {

    cache = {};

    // Returns the weekday number for the given date relative to January 1, 1970.
    function weekday(date) {
    c = cache[date];
    if (c != null) {
    return c;
    }

    var weekdays = weekdayOfYear(date),
    year = date.getFullYear();
    while (--year >= 1970) weekdays += weekdaysInYear(year);

    cache[date] = weekdays;
    cache[weekdays] = date;

    return weekdays;
    }

    //multiplier to go from weekday number to miliseconds (javascript timestamp)
    weekday.factor = 864e5;

    // Returns the date for the specified weekday number relative to January 1, 1970.
    weekday.invert = function(weekdays) {
    c = cache[weekdays];
    if (c != null) {
    return c;
    }

    var year = 1970,
    yearWeekdays;

    // Compute the year.
    while ((yearWeekdays = weekdaysInYear(year)) <= weekdays) {
    ++year;
    weekdays -= yearWeekdays;
    }

    // Compute the date from the remaining weekdays.
    var days = weekdays % 5,
    day0 = ((new Date(year, 0, 1)).getDay() + 6) % 7;
    if (day0 + days > 4) days += 2;

    date = new Date(year, 0, (weekdays / 5 | 0) * 7 + days + 1);

    cache[date] = weekdays;
    cache[weekdays] = date;

    return date;
    };

    // Returns the number of weekdays in the specified year.
    function weekdaysInYear(year) {
    return weekdayOfYear(new Date(year, 11, 31)) + 1;
    }

    // Returns the weekday number for the given date relative to the start of the year.
    function weekdayOfYear(date) {
    var days = d3.time.dayOfYear(date),
    weeks = days / 7 | 0,
    day0 = (d3.time.year(date).getDay() + 6) % 7,
    day1 = day0 + days - weeks * 7;
    return Math.max(0, days - weeks * 2
    - (day0 <= 5 && day1 >= 5 || day0 <= 12 && day1 >= 12) // extra saturday
    - (day0 <= 6 && day1 >= 6 || day0 <= 13 && day1 >= 13)); // extra sunday
    }

    return weekday;

    })();
  9. mayo revised this gist May 13, 2014. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion README.md
    Original file line number Diff line number Diff line change
    @@ -1 +1 @@
    Expanding on mbostock's [weekday.js](https://gist.github.com/mbostock/5827353) and mayo's [Weekdays](https://gist.github.com/mayo/c0c1ca7f5bbb30cc3ef3) gists by adding adaptive tick mark format and wrapping it all up in a `dayselect` scale (d3.scale.dayselect).
    Expanding on mbostock's [weekday.js](https://gist.github.com/mbostock/5827353) and my [Weekdays](https://gist.github.com/mayo/c0c1ca7f5bbb30cc3ef3) gists by adding adaptive tick mark format and wrapping it all up in a `dayselect` scale (d3.scale.dayselect).
  10. mayo revised this gist May 13, 2014. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion README.md
    Original file line number Diff line number Diff line change
    @@ -1 +1 @@
    Expanding on mbostock's [weekday.js](https://gist.github.com/mbostock/5827353) and [Weekdays](https://gist.github.com/mayo/c0c1ca7f5bbb30cc3ef3) gists by adding adaptive tick mark format and wrapping it all up in a `dayselect` scale (d3.scale.dayselect).
    Expanding on mbostock's [weekday.js](https://gist.github.com/mbostock/5827353) and mayo's [Weekdays](https://gist.github.com/mayo/c0c1ca7f5bbb30cc3ef3) gists by adding adaptive tick mark format and wrapping it all up in a `dayselect` scale (d3.scale.dayselect).
  11. mayo created this gist May 13, 2014.
    1 change: 1 addition & 0 deletions README.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1 @@
    Expanding on mbostock's [weekday.js](https://gist.github.com/mbostock/5827353) and [Weekdays](https://gist.github.com/mayo/c0c1ca7f5bbb30cc3ef3) gists by adding adaptive tick mark format and wrapping it all up in a `dayselect` scale (d3.scale.dayselect).