more-things-you-probably-don't-need-to-know

[Update: I've converted this blog to Octopress, which makes this stuff much easier to deal with. But the code below should still work on WordPress.]

[Update 2: This site is now generated with Harp.js rather than Octopress. The above still applies.]

I started this blog in large part to get down and dirty and technical. Not just to write essays, but to take full advantage of the medium of the web and exchange ideas through illustrations and code. And not just static images, although images can be powerful, but through interactive widgets that are fun to engage with. For example:

[ figure 1 ]
[ figure 2 ]
[ figure 3 ]

To do this I needed a way to embed custom JavaScript code into a Wordpress post. The standard method doesn't really fit with my workflow, and seems pretty clunky besides. Last Sunday I finally got around to sitting down and figuring out a way (while procrastinating for something else, of course) and today I spent a little bit of time refining it and writing it up.

Embedding: The <script> Tag

You can just use <script>. Unfortunately Wordpress likes to add <p> tags in strange places wherever text has double line breaks. This method on Grant Skinner's blog seemed promising, but upon investigation I couldn't get it to work at all. Maybe it's just my particular setup or melange of plugins.

I can get around the problem by running the code through jsmin, which removes comments and extraneous whitespace. This means that you'll probably want to edit your code in a local file and keep it around as a single point of truth. Embed in <script> tags and insert into the "Text" tab of your post and you're golden:

<script>// <![CDATA[
console.log("===== Count and Join, version 1");var nums=[];for(var i=0;i<100;++i)
{nums.push(i);}
console.log(nums.join("|"));console.log("=====");
// ]]></script>

(You may wish to check your browser's JavaScript console to verify that the above code was executed.)

Ampersands!

But... encoding problems lurk. For some reason Wordpress encodes ampersands inside <script> tags as &#038; in the output HTML, so the following code leaves an error in the JavaScript log (“Uncaught SyntaxError: Unexpected token ILLEGAL” in Chrome):

<script>// <![CDATA[
console.log("This will break:");
console.log(true && true);
// ]]></script>

You can get around this by using De Morgan's law to avoid ampersands altogether, but this is a hackier hack than I'm willing to perpetrate. Plus if you're using bitwise operators you're on your own. I also didn't want to disable wptexturize altogether — I actually like what it does outside of my code blocks.

Let's try another variant of gskinner's <pre> trick, but this time let's do something a little sneakier. We start by specifying the code like so:

<pre id="some_non_breaking_code" style="display:none;">
console.log("This will not break:");
console.log(true && true);
</pre>

And then add a tiny bit of script that pulls out the contents of that block, fixes its ampersands (now encoded as &), and executes it:

<script type="text/javascript">// <![CDATA[
eval(jQuery("#some_non_breaking_code").html().replace(/\u0026amp;/g, '\u0026'));
// ]]></script>

This has the added benefit that we no longer have to use jsmin, so the count-and-join example above now looks like this:

<pre id="count_and_join_2" style="display:none;">
// Hey, a comment!
console.log("===== Count and Join, version 2");
var nums=[];

for(var i = 0; i < 100; ++i)
{
    nums.push(i);
}

console.log(nums.join("|"));
console.log("=====");
</pre>

We're using gskinner's method of hiding the code block with CSS, so we have to add style="display:none;".

If you're using the Crayon Syntax Highlighter plugin, you'll need to uncheck its "Capture <pre> tags as Crayons" option and go back to using shortcodes. Sorry, nothing's perfect.

External Scripts

Those graphs above are built with D3.js, which needs to be loaded in its own <script> tag. Wordpress doesn't like it when you specify a src parameter to <script> tags within a post, so what to do? I tried a few tricks, like dynamically adding the tag to the document's <head>, but that doesn't work. You might be able to find a Wordpress plugin that loads JavaScript libraries (if you do, let me know), but I ended up adding a line to my child theme's header.php:

<?php wp_enqueue_script("d3", "http://d3js.org/d3.v3.min.js"); ?>

And then after a little searching I discovered jQuery's getScript. Since not all my posts use D3.js, I replaced the above line with a call in the embedded JavaScript:

jQuery.getScript("http://d3js.org/d3.v3.min.js", /* callback */);

Ah, much better.

The Whole Shebang

If you're curious, here's everything working together. First comes the code:

<pre id="binomials" style="display:none;">
function clamp(val, low, hi)
{
    return Math.max(Math.min(val, hi), low);
}

var factDict = {};
function fact(N)
{
    if (N === 0)
    {
        return 1;
    }
    if (factDict[N])
    {
        return factDict[N];
    }
    factDict[N] = N * fact(N - 1);
    return factDict[N];
}

function choose(N, k)
{
    return fact(N) / (fact(k) * fact(N - k));
}

function probPow(p, i)
{
    return i == 0 ? 1 : Math.pow(p, i);
}

function buildFigure(element)
{
    var lineData = [];

    function setLine(key, value)
    {
        for (var idx = 0; idx < lineData.length; ++idx)
        {
            if (lineData[idx].key == key)
            {
                lineData[idx].value = value;
                return;
            }
        }
        lineData.push({key: key, value: value});
    }

    function getLine(key)
    {
        for (var idx = 0; idx < lineData.length; ++idx)
        {
            if (lineData[idx].key == key)
            {
                return lineData[idx].value;
            }
        }
        return "";
    }

    var N = 30;
    if (jQuery(element).attr("N"))
    {
        N = Math.round(jQuery(element).attr("N"));
    }
    setLine("N", N);

    var p = 0.05;
    if (jQuery(element).attr("p"))
    {
        p = jQuery(element).attr("p");
    }
    setLine("p", p);

    jQuery(element).html("");

    var width = 480;
    var height = 120;
    var margin = 5;

    var svg = d3.select(element).append("svg:svg");
    svg.attr("width", width);
    svg.attr("height", height);

    var bg = svg.append("rect");
    bg.attr("width", width);
    bg.attr("height", height);
    bg.style('fill', 'white');
    bg.style('stroke', 'black');

    var g = svg.append("g");

    var xScale = d3.scale.ordinal().rangeBands([margin, width - margin], 0.1);
    var yScale = d3.scale.linear().range([height - margin, margin]);

    var mousePt = [-1, -1];

    function update()
    {
        if (mousePt[0] >= 0 && mousePt[1] >= 0)
        {
            p = clamp((mousePt[0] - margin) / (width - 2*margin), 0, 1);
            setLine("p", Math.round(100 * p) / 100);
        }

        var data = d3.range(N + 1).map(function (i)
        {
            return choose(N, i) * probPow(p, i) * probPow(1 - p, N - i);
        });

        xScale.domain(data.map(function (d, i) { return i; }));
        yScale.domain([0, d3.max(data)]);

        var bars = g.selectAll(".bar")
            .data(data)
            ;
        bars.transition()
            .duration(0)
            .attr("y", function (d) { return yScale(d); })
            .attr("height", function (d) { return height - margin - yScale(d); })
            ;
        bars.enter().append("rect")
            .attr("class", "bar")
            .attr("x", function (d, i) { return xScale(i); })
            .attr("width", xScale.rangeBand())
            .attr("y", function (d) { return yScale(d); })
            .attr("height", function (d) { return height - margin - yScale(d); })
            .style('fill', 'steelBlue')
            ;

        var texts = svg.selectAll("text")
            .data(lineData, function (d) { return d.key; })
            ;
        texts.transition()
            .duration(0)
            .text(function (d, i) { return d.key + ": " + d.value; })
            ;
        texts.enter().append("text")
            .text(function (d, i) { return d.key + ": " + d.value; })
            .attr("x", 5)
            .attr("y", function (d, i) { return 15 + i * 20; })
            ;
        texts.exit().remove();
    }
    update();

    svg.on("mousemove", function ()
    {
        mousePt = d3.mouse(this);
        update();
    });
}

jQuery(document).ready(function($)
{
    jQuery.getScript("http://d3js.org/d3.v3.min.js", function (data, textStatus, jqxhr)
    {
        $(".figure").each(function (i, value) { buildFigure(value); });
    });
});
</pre>

Then the placeholders in the DOM (yes, those are non-standard parameters and they won't validate — I'll fix that later):

<div class="figure">[ figure 1 ]</div>
<div class="figure" N="100" p="0.5">[ figure 2 ]</div>
<div class="figure" N="10" p="0.6">[ figure 3 ]</div>

Then the JavaScript snippet that kicks it off (note the special handling of greater than and less than signs):

<script type="text/javascript">// <![CDATA[
function runNamedScript(sName) { eval(jQuery("#" + sName).html().replace(/\u0026amp;/g, '\u0026').replace(/\u0026lt;/g, '\u003c').replace(/\u0026gt;/g, '\u003e')); }
runNamedScript("binomials");
// ]]></script>

Magic!

[ figure 4 ]

comments powered by