Monthly Archive for January, 2008

And Again!

Another update to FlexChart this evening, providing set and series colors (both fills and strokes), an option for including/excluding the legend when exporting a chart to PNG, and a few new examples showcasing the features (including some really ugly developer art).

With the coloring support, the 'Grouped Series' example makes a lot more visual sense, so if you thought WTF yesterday, go look at it again.  ;)

More FlexCharts Goodness

Another batch of changes to FlexChart.

First is grouped series, which is hard to explain, but easy to understand.  Go hit the demo and select "Grouped Series" from the dropdown.  The chart layout has always been possible, but the legend just displayed a flat series list with no awareness of the groupings.  To see the old behaviour, change the 'legendStyle' attribute of the chart tag to "simple", and hit "Update Chart".

Second is direct support for PNG exports of the chart.  A Base64-encoded PNG can be requested via an ExternalInterface callback which you can then do whatever you want with.  The demo app just sends it server side (in a new window) for deserialization with CFIMAGE and immediately sends it back to the client.  Hardly interesting, but definitely illustrative.

Finally, I've abandoned the compile-in-page model, Flex SDK 2.0.1, and a single file architecture completely.  You can drop the binary anywhere you want without any dependency on FlexBuilder, a Flex SDK, or any server component, which is nice.  Of course, if you want to customize, it required FlexBuilder 3.  I think that's the right tradeoff.  I've also started splitting up the engine for easier maintenance, now that there's no need to keep it single file (which there was when it was server-compiled).  I haven't done much, but that'll come.  Definitely make it easier to work with.

Soon to come down the pipe are custom colors for series and sets, support for multiple y-axes, and a grid legend style which is basically a headerless datagrid under the chart with a row per series and a column per x-axis item, all lined up evenly.

FlexChart Update

Long time no blog…  I've updated my FlexChart component slightly, as well as repackaged it for easier consumption.  The new feature is the availability of a 'dataTipFunction' attribute on the root 'chart' element, which will be called to format data tips.  It gets passed an object with various keys about the backing chart item.  Since that's all it gets (rather than a HitData instance), it's not as powerful as the native Flex capabilities, but it gets the job done with pure JS.

The repackaging was to a binary (precompiled) format, rather than requiring the server compilation I'd originally built it with.  The old style is still available where it always has been, but the preferred usage is now either the distribution directory (svn) or the FlexBuilder project itself.  With the new model, you just drop the compiled binaries (yours or mine) into any web-accessible directory and use them, regardless of whether you have LCDS installed to do the compilation.

Checkbox Range Selection (a la GMail)

If any of you use GMail, you'll know that you can shift click the checkboxes on the conversation list to select a range of conversations (i.e. click the second conversation's checkbox and then shift-click the tenth conversation's checkbox). You can also deselect the same way (click the seventh, and then shift-click the fourth). Finally, you can shift-click a second time (third click total) to extend the range. I wanted that functionality in one of my apps, and here it is.

Update: I've repackaged the code as a jQuery plugin based on Dan Switzer's suggestion. I've left the original code in place, just struck it out.

function configureCheckboxesForRangeSelection(spec) {
  var lastCheckbox = null;
  jQuery(function($) { // for Prototype protection
    var $spec = $(spec);
    $spec.bind("click", function(e) {
      if (lastCheckbox != null && e.shiftKey) {
        $spec.slice(
          Math.min($spec.index(lastCheckbox), $spec.index(e.target)),
          Math.max($spec.index(lastCheckbox), $spec.index(e.target)) + 1
        ).attr({checked: e.target.checked ? "checked" : ""});
      }
      lastCheckbox = e.target;
    });
  }); // for Prototype protection
};

(function($) {
  $.fn.enableCheckboxRangeSelection = function() {
    var lastCheckbox = null;
    var $spec = this;
    $spec.bind("click", function(e) {
      if (lastCheckbox != null && e.shiftKey) {
        $spec.slice(
          Math.min($spec.index(lastCheckbox), $spec.index(e.target)),
          Math.max($spec.index(lastCheckbox), $spec.index(e.target)) + 1
        ).attr({checked: e.target.checked ? "checked" : ""});
      }
      lastCheckbox = e.target;
    });
  };
})(jQuery);

It requires jQuery to be available (I used 1.2.1 and 1.2.3), and is safe to use with Prototype also in-scope (regardless of which owns the $ function). Call that function passing in a jQuery expression (NOT a jQuery object) that describes the checkboxes you want to be range-selectable:

configureCheckboxesForRangeSelection("input.image-checkbox");
$("input.image-checkbox").enableCheckboxRangeSelection();

Thanks to Matt Wood (a coworker) for the slice/index suggestion. My initial implementation had used an each with a conditional and a status variable - definitely less elegant.  Check the project page for any additional updates.

Report/Query DSLs Update

I've posted a simple demo app for both DSLs that you can play with. It's included in the distribution as index.cfm, so you'll get it if you pull down the source from SVN. I've also created a readme.txt file in the distribution with the text of my intro blog post.

I also made a very minor (though backwards incompatible) update to my Query DSL implementation. There is a convertToSimpleCriterion method, designed to be overridden by subclasses if necessary. The initial implementation contained the logic for processing negated terms (those prefixed with '-' or '!'), and for splitting terms into predicate/value pairs. Since that's part of the DSL implementation's job, that needed to b done by the internal methods, so the behaviour would be inherited by any subclasses. I've changed the convertToSimpleCriterion method to be passed all three parts individually, so the parsing can live inside the core of the implementation.

Report/Query DSLs

I use a pair of stacked DSLs (Domain Specific Languages) for searching, reporting, and goal management in a couple applications. A discussion at work (with Joshua and Koen) provided the solution for the final piece I wanted to implement before releasing them. The first layer is for parsing query strings, and the latter for using query strings to build up report structures and such.

For the impatient, code is available at https://ssl.barneyb.com/svn/barneyb/reporting_dsl/trunk/. You want to read the docs in querydsl.cfc and reportdsl.cfc.

The Query DSL is quite simple:

  • spaces delimit terms
  • terms are ANDed together
  • terms can be quoted if they contain spaces
  • terms can contain a prefix (predicate) separated from the value by a colon
  • a leading dash (or exclamation point) can be used to negate a term
  • the OR keyword (caps mandatory) can be used to create alternatives
  • parentheses can be used to group terms

Here's some examples:

  • cat type:pet
  • cat -manx
  • cat OR dog
  • cat (-manx type:"house pet") OR dog

The last item is not possible with Google Search constructs, and is the only construct available that Google doesn't provide. Which isn't to suggest that the DSL supports the gamut of features Google supports.

The result of using the DSL parser is a graph of 'criterion' instances. For example 'andcriterion', 'orcriterion', or 'simplecriterion'. The second example above would return an 'andcriterion' instance with it's left side pointing to a 'simplecriterion' instance for "cat", and the right side pointing to another (negated) 'simplecriterion' instance for "manx". More complex expression result in more complex graphs, but they graph is always singly rooted.

The Report DSL is a bit more complex, and leverages the Query DSL. It is line oriented, with a single-character command specifier at the beginning of each line. A document contains one or more "condition" lines (prefixed with a '+' or an '&') which are comprised of a value and a query string. A document may also have one or more "globals" lines (prefixed with a '+') that only contain a query string, and which is added to each condition's query string (to reduce duplication). Here's a sample document:

+ tag:dining -tag:home
? McDonalds : tag:mcdonalds
? Jack in the Box : tag:"jack in the box"
? Cheesecake Factory : tag:"cheesecake factory"
& Fast Food : tag:"fast food"
& Total : *

There is no real difference between the '?' and '&' lines (referred to as conditions and aggregates respectively), the two variants are there to provide a built-in differentiator. Here I'm using them as their names imply: the conditions are raw conditions, and the aggregates aggregate the conditions together. If this report document were used to generate a chart, you might see the conditions as column series and the aggregates as line series.

The result of parsing a document is a structure with 'coreCriterion', 'conditions' and 'aggregates' keys, all of which are optional if the document doesn't specify anything to fill them. The former holds a criterion graph (since it's just a query string). The latter two are arrays of structs, where each struct contains 'value', 'query', and 'criterion' keys. The first two are simply the two halves of the corresponding line of the document, and the 'criterion' key holds the parsed query string.

So what is this good for? By itself, not much, but with a method for converting between a criterion graph and a SQL expression, the Query DSL will give you an easy way to build very granular search functionality on your site. Once you have that, leveraging the Report DSL allows creation of fairly complex reports with a minimum of fuss (do convert each condition's criterion to SQL, run your query, and feed it into a chart series). Of course, the output needn't be a chart, but that's a good example.

What's most important here is that both of these DSLs are for users to use, not coders. I.e. strings/documents are created by users to customize the behaviour of your application, allowing them to build very specific reports very easily. And the definitions for those reports are simple strings, which makes persisting them a breeze.

With a little more creativity, you can get other behaviour. I alluded to goal management up top. If you were to take the report document above, remove the aggregates (the '&' lines), and replace the values (restaurant names) with integers, according to how desirable they are, you'd have a way to rank given dining adventures. Write yourslef a method for converting the parsed document into a SQL CASE..END statement (leveraging your criterion-to-SQL method, of course), you have a really easy way to assign a "points" value to the items you're querying. Wrap that with some sort of structure for managing a points-per-time-period structure, and you have a very flexible and easy to use goal tracking system.

Like so much else, I don't know how useful this is for others. The Query DSL probably is, but the Report DSL might not be. But it's made my life much easier, so worth the effort I've put into it. Code is only available via Subversion (browser or SVN client) at the URL above. All paths are relative, so you can svn:externals (use a revision number!) it into your existing package-space and reference the CFCs natively. There are docs and more examples in the files as well.