From 93c7c941f44d677c053d672cfbc04fd99de4c4ab Mon Sep 17 00:00:00 2001 From: Ole Laursen Date: Tue, 21 Jun 2011 16:43:17 +0200 Subject: [PATCH] New categories plugin with support for plotting categories/textual data directly; also tick generators now get the whole axis rather than just min/max --- API.txt | 2 +- NEWS.txt | 5 + examples/categories.html | 40 ++++++++ examples/index.html | 2 +- jquery.flot.categories.js | 189 ++++++++++++++++++++++++++++++++++++++ jquery.flot.js | 4 +- 6 files changed, 238 insertions(+), 4 deletions(-) create mode 100644 examples/categories.html create mode 100644 jquery.flot.categories.js diff --git a/API.txt b/API.txt index 5c22382..61d2f34 100644 --- a/API.txt +++ b/API.txt @@ -187,7 +187,7 @@ Customizing the axes transform: null or fn: number -> number inverseTransform: null or fn: number -> number - ticks: null or number or ticks array or (fn: range -> ticks array) + ticks: null or number or ticks array or (fn: axis -> ticks array) tickSize: number or array minTickSize: number or array tickFormatter: (fn: number, object -> string) or string diff --git a/NEWS.txt b/NEWS.txt index 5382e51..0837919 100644 --- a/NEWS.txt +++ b/NEWS.txt @@ -25,6 +25,11 @@ Changes: - Support for multiple thresholds in thresholds plugin (patch by Arnaud Bellec, issue 523). +- Support for plotting categories/textual data directly with new + categories plugin. + +- Tick generators now get the whole axis rather than just min/max. + Bug fixes - Fix problem with null values and pie plugin (patch by gcruxifix, diff --git a/examples/categories.html b/examples/categories.html new file mode 100644 index 0000000..05d8e17 --- /dev/null +++ b/examples/categories.html @@ -0,0 +1,40 @@ + + + + + Flot Examples + + + + + + + +

Flot Examples

+ +
+ +

With the categories plugin you can plot categories/textual data + easily.

+ + + + + diff --git a/examples/index.html b/examples/index.html index f24f750..ef85363 100644 --- a/examples/index.html +++ b/examples/index.html @@ -12,7 +12,7 @@ diff --git a/jquery.flot.categories.js b/jquery.flot.categories.js new file mode 100644 index 0000000..f3cfc7e --- /dev/null +++ b/jquery.flot.categories.js @@ -0,0 +1,189 @@ +/* +Flot plugin for plotting textual data or categories. Consider a +dataset like [["February", 34], ["March", 20], ...]. This plugin +allows you to plot such a dataset directly. + +To enable it, you must specify mode: "categories" on the axis with the +textual labels, e.g. + + $.plot("#placeholder", data, { xaxis: { mode: "categories" } }); + +By default, the labels are ordered as they are met in the data series. +If you need a different ordering, you can specify "categories" on the +axis options and list the categories there: + + xaxis: { + mode: "categories", + categories: ["February", "March", "April"] + } + +If you need to customize the distances between the categories, you can +specify "categories" as an object mapping labels to values + + xaxis: { + mode: "categories", + categories: { "February": 1, "March": 3, "April": 4 } + } + +If you don't specify all categories, the remaining encountered +categories will be numbered from the max value plus 1 (with a spacing +of 1 between each). + + +Internally, the plugin works by transforming the input data through an +auto-generated mapping where the first category becomes 0, the second +1, etc. Hence, a point like ["February", 34] becomes [0, 34] +internally in Flot (this is visible in hover and click events that +return numbers rather than the category labels). The plugin also +overrides the tick generator to spit out the categories as ticks +instead of the values. + +If you need to map a value back to its label, the mapping is always +accessible as "categories" on the axis object, e.g. +plot.getAxes().xaxis.categories. +*/ + +(function ($) { + var options = { + xaxis: { + categories: null + }, + yaxis: { + categories: null + } + }; + + function processRawData(plot, series, data, datapoints) { + // if categories are enabled, we need to disable + // auto-transformation to numbers so the strings are intact + // for later processing + + var xCategories = series.xaxis.options.mode == "categories", + yCategories = series.yaxis.options.mode == "categories"; + + if (!(xCategories || yCategories)) + return; + + var format = datapoints.format; + + if (!format) { + // FIXME: auto-detection should really not be defined here + var s = series; + format = []; + format.push({ x: true, number: true, required: true }); + format.push({ y: true, number: true, required: true }); + + if (s.bars.show || (s.lines.show && s.lines.fill)) { + format.push({ y: true, number: true, required: false, defaultValue: 0 }); + if (s.bars.horizontal) { + delete format[format.length - 1].y; + format[format.length - 1].x = true; + } + } + + datapoints.format = format; + } + + for (var m = 0; m < format.length; ++m) { + if (format[m].x && xCategories) + format[m].number = false; + + if (format[m].y && yCategories) + format[m].number = false; + } + } + + function getNextIndex(categories) { + var index = -1; + + for (var v in categories) + if (categories[v] > index) + index = categories[v]; + + return index + 1; + } + + function categoriesTickGenerator(axis) { + var res = []; + for (var label in axis.categories) { + var v = axis.categories[label]; + if (v >= axis.min && v <= axis.max) + res.push([v, label]); + } + + res.sort(function (a, b) { return a[0] - b[0]; }); + + return res; + } + + function setupCategoriesForAxis(series, axis, datapoints) { + if (series[axis].options.mode != "categories") + return; + + if (!series[axis].categories) { + // parse options + var c = {}, o = series[axis].options.categories || {}; + if ($.isArray(o)) { + for (var i = 0; i < o.length; ++i) + c[o[i]] = i; + } + else { + for (var v in o) + c[v] = o[v]; + } + + series[axis].categories = c; + } + + // fix ticks + if (!series[axis].options.ticks) + series[axis].options.ticks = categoriesTickGenerator; + + transformPointsOnAxis(datapoints, axis, series[axis].categories); + } + + function transformPointsOnAxis(datapoints, axis, categories) { + // go through the points, transforming them + var points = datapoints.points, + ps = datapoints.pointsize, + format = datapoints.format, + formatColumn = axis.charAt(0), + index = getNextIndex(categories); + + for (var i = 0; i < points.length; i += ps) { + if (points[i] == null) + continue; + + for (var m = 0; m < ps; ++m) { + var val = points[i + m]; + + if (val == null || !format[m][formatColumn]) + continue; + + if (!(val in categories)) { + categories[val] = index; + ++index; + } + + points[i + m] = categories[val]; + } + } + } + + function processDatapoints(plot, series, datapoints) { + setupCategoriesForAxis(series, "xaxis", datapoints); + setupCategoriesForAxis(series, "yaxis", datapoints); + } + + function init(plot) { + plot.hooks.processRawData.push(processRawData); + plot.hooks.processDatapoints.push(processDatapoints); + } + + $.plot.plugins.push({ + init: init, + options: options, + name: 'categories', + version: '1.0' + }); +})(jQuery); diff --git a/jquery.flot.js b/jquery.flot.js index 4a94e31..0db9fe1 100644 --- a/jquery.flot.js +++ b/jquery.flot.js @@ -1376,7 +1376,7 @@ // we might need an extra decimal since forced // ticks don't necessarily fit naturally - if (axis.mode != "time" && opts.tickDecimals == null) { + if (!axis.mode && opts.tickDecimals == null) { var extraDec = Math.max(0, -Math.floor(Math.log(delta) / Math.LN10) + 1), ts = generator(axis); @@ -1403,7 +1403,7 @@ else if (oticks) { if ($.isFunction(oticks)) // generate the ticks - ticks = oticks({ min: axis.min, max: axis.max }); + ticks = oticks(axis); else ticks = oticks; }