PHP - ImageMagick command line Pie Chart

This code was converted from a bash version to create pie charts. It is much faster and does not require bc on the server.

PHP is used to create the ImageMagick command line.

This code creates pie chart and legend images which can then be placed on a web page or added in to a document.

To call the code, create an array of values, instantiate the GFX objects, establish the label_color_map as an associative array where the label text is the key and the value is a hex color code, and finally create the chart, sending a name and the data array, like so:


                        $data = array(50, 70, 100, 3, 49);
                        $chart = new GFX();
                        $chart->label_color_map(array('one' => '#1c28a1', 
                          'two' => '#600087',
                          'three' => '#107a3f',
                          'four' => '#bf0000',
                          'five' => '#ff6d00'));
                        $chart->piechart('project',$data);

<?php 
Class GFX extends Base {

        public function __construct($parms = null) {
                $this->_properties = array('centerx','centery','width','height',
'radius');
                if (!is_array($parms)) {
                        $parms['width'] = $parms ['height'] = 330;
                        $parms['centerx'] = $parms['centery'] = 160;
                        $parms['radius'] = 135;
                }
                parent::__construct($parms);
        }

        public function label_color_map ($map = array()) {
                $this->_data['labels'] = array();
                foreach ($map as $k => $v) {
                        $this->_data['labels'][$k] = $v;
                }
        }
        public function piechart($name = null, $data = array()) {
                // Thanks to: http://jbkflex.wordpress.com/2011/07/28/creating-a-svg-pie-chart-html5/
                if ($name === null) return;

                $total = array_sum($data);

                $max=0;$kmax='None';
                foreach ($data as $k => $v) {
                        $value[$k] = (int)(($v/$total) * 100);
                        if ($value[$k] > $max) {
                                $kmax=$k;
                                $max=$value[$k];
                        }
                }
                while (array_sum($value) < 100) {
                        $value[$kmax]++;
                }

                foreach ($data as $k => $v) {
                        $value[$k] = (int)(($v/$total) * 100);
                        if ($value[$k] > $max) {
                                $kmax=$k;
                                $max=$value[$k];
                        }
                }
                while (array_sum($value) < 100) {
                        $value[$kmax]++;
                }
                $centerx = $this->_data['centerx'];
                $centery = $this->_data['centery'];
                $radius= $this->_data['radius'];
                $count=0;
                $startAngle=0;
                $endAngle=0;
                $arc=0;
                $x1=0;
                $x2=0;
                $y1=0;
                $y2=0;
                $pi=pi();
                $cmd='convert -size '.$this->_data['width'].'x'.$this->_data['height'].' xc:white -stroke white -strokewidth 5 ';

                foreach ($value as $k => $v) {
                        $startAngle=$endAngle;
                        $endAngle=$startAngle+(360*$v/100);
                        $theta = $pi*$startAngle/180;
                        $x1=$centerx+$radius*cos($theta);
                        $y1=$centery+$radius*sin($theta);
                        $theta = $pi*$endAngle/180;
                        $x2=$centerx+$radius*cos($theta);
                        $y2=$centery+$radius*sin($theta);
                        $arc = ($v >= 50) ? '1' : '0';
                        $cmd.=' -fill "'.$this->_data['labels'][$k].'" -draw "path \'M '.$centerx.','.$centery.' L '.$x1.','.$y1.
                                ' A '.$radius.','.$radius.' 0 '.$arc.',1 '.$x2.','.$y2.' Z"';
                        $count++;
                }
                $cmd.=' '.escapeshellarg(_GFX_DIR_.'/'.$name.'_chart.jpg');
                `$cmd`;

                if (!is_array($this->_data['labels'])) return;

                $KEY_SIZE=20;
                $MARGIN=5;
                $TEXT_X=$KEY_SIZE+$MARGIN;

                $height =  $TEXT_X * count($value);

                $cmd = 'convert -size 135x'.$height.' xc:white -fill white ';
                $label = " -font 'Nimbus-Sans-Bold' -stroke none -pointsize 12 ";
                $count=0;
                $y1=5;
                foreach ($value as $k => $v) {
                        $y2=$y1+$KEY_SIZE;
                        $y3=$y2-$MARGIN;
                        $label.=' -fill "'.$this->_data['labels'][$k].'" -draw "rectangle 0,'.$y1.' '.$KEY_SIZE.','.$y2.'"'.
                                ' -draw "text '.$TEXT_X.','.$y3.' '.escapeshellarg(/*$data[$k].' '.*/$v.'% '.$k).'"';
                        $count++;
                        $y1=$y1+$KEY_SIZE+$MARGIN;
                }
                $cmd.= $label.' '.escapeshellarg(_GFX_DIR_.'/'.$name.'_legend.jpg');
                `$cmd`;
        }
}

<?php
Abstract Class Base {

        protected $_properties;
        protected $_data;
        protected $_valid = false;
        protected $_error_message = array();

        function __construct($props) {
                $this->_data = array_fill_keys($this->_properties, null);
                $this->_data = array_merge($this->_data, $props);
        }

        function __get($property) {
                return isset($this->_data[$property]) ? $this->_data[$property] : null;
        }

        function __set($property, $value) {
                $this->_data[$property] = $value;
        }

        function isValid($property = null) {
                if ($property === null)
                        return $this->_valid || (count($this->_error_message) == 0);
                else
                        return !isset($this->_error_message[$property]);
        }

        function error($property) {
                if (isset($this->_error_message[$property])) {
                        return $this->_error_message[$property];
                } else {
                        return '';
                }
        }
}

Many thanks to the link above for the chart algorithm.

This post courtesy of Worktrainer.

bash - ImageMagick - Pie Chart Script

This is a very simple script that will create a pie chart and legend using bash, bc, and ImageMagick. Many thanks to the link above which provided the algorithms to complete the SVG paths.

It accepts a list of parameters which must add up to 100. The script has two arrays, LABELS and COLORS which are applied to each parameter in order.


#!/bin/bash

function usage {
        echo "Usage: `basename $0` piece1 piece2 piece3 ..."
        echo -e "\tWhere each piece is a percentage of the pie"
        echo -e "\tThe pieces must add up to 100"
        exit $E_BADARGS
}

LABELS=("None"  "Accept" "Defer" "Discard")
COLORS=("#1c28a1"  "#107a3f" "#ff6d00" "#bf0000");

TARGET_DIR='images'

if [ $# -lt 3 ];
then
        usage
fi;

arc=()
sum=0
for piece in "$@"
do
        sum=$(( $piece + $sum ))
done

if [ $sum -ne 100 ];
then
        usage
fi

WIDTHxHEIGHT='330x330'
RADIUS=135
CENTERX=160
CENTERY=160

count=0
startAngle=0
endAngle=0
arc=0
total=0
x1=0
x2=0
y1=0
y2=0
pi=$(echo "scale=10; 4*a(1)" | bc -l)
cmd='convert -size '$WIDTHxHEIGHT' xc:white -stroke white -strokewidth 5 '
first=0
for piece in "$@"
do
        startAngle=$endAngle
        endAngle=$(echo "scale=10;$startAngle+(360*$piece/100)" | bc -l)
        x1=$(echo "scale=10;$CENTERX+$RADIUS*c($pi*$startAngle/180)" | bc -l)
        y1=$(echo "scale=10;$CENTERY+$RADIUS*s($pi*$startAngle/180)" | bc -l)
        x2=$(echo "scale=10;$CENTERX+$RADIUS*c($pi*$endAngle/180)" | bc -l)
        y2=$(echo "scale=10;$CENTERY+$RADIUS*s($pi*$endAngle/180)" | bc -l)
        if [ $piece -ge 50 ]
        then
                FIFTY=1
        else
                FIFTY=0
          fi
        cmd=$cmd"-fill '${COLORS[count]}' -draw \"path 'M $CENTERX,$CENTERY L $x1,$y1 A $RADIUS,$RADIUS 0 $FIFTY,1 $x2,$y2 Z'\" "

        count=$(( $count + 1 ))
done
cmd=$cmd" $TARGET_DIR/idea_chart.jpg"

eval $cmd

KEY_SIZE=20
MARGIN=5
TEXT_X=$(( $KEY_SIZE+$MARGIN ))

legends=$(( $#*($KEY_SIZE+$MARGIN) ))

cmd='convert -size 125x'$legends' xc:white -fill white '
label=" -font 'Nimbus-Sans-Bold' -stroke none -pointsize 12 "
count=0;
y1=5
for piece in "$@"
do
        y2=$(( $y1+$KEY_SIZE ))
        y3=$(( $y2-$MARGIN ))
        label=$label"-fill '${COLORS[count]}' -draw 'rectangle 0,$y1 $KEY_SIZE,$y2 ' -draw \"text $TEXT_X,$y3 '$piece% ${LABELS[count]}'\" "
        count=$(( $count + 1 ))
        y1=$(( $y1+$KEY_SIZE+$MARGIN ))
done
cmd=$cmd$label" $TARGET_DIR/idea_legend.jpg"

eval $cmd

exit; 

The FIFTY is used when an arc will span 50% or more of the pie, it sets the large-arc-flag.

This post courtesy of Worktrainer.

dgrid Print Shim

If you need to print a dgrid, you can query the store and create an HTML table with JavaScript.

Create and work with the dgrid as usual, but add a second div below it which will contain the table.


<div class="no-print" id="grid"></div>
<div class="print-only" id="grid_print"></div>

Then set up the CSS to hide the print-only by default. Create a print.css file and the appropriate media attribute hide the no-print class and display the print-only.

Relevant lines of print.css

.print-only {
        display: block;
}
.print-only table {
        width: 100%;
        table-layout: fixed;
}
.print-only table th {
        border: 1px solid #000;
        font-weight: bolder;
}

Relevant lines of admin.css (screen)


.print-only,.page-break {
        display:none;
}

Link tag for the print.css file

<link media="print” href=’/css/print.css’ rel=’stylesheet’ type=’text/css’>

JavaScript to create the table. This code is used on a page with several grids, the grids are in the grids object, with stores in the stores object.


                byId("print_button").onclick = function() {
                                /* This will find all the print_grids */
                                query("[id$='_grid_print']").forEach(function(node) {
                                                if (node.hasChildNodes()) {
                                                        node.removeChild(node.firstChild);
                                                }

                                                var grid, grid_id;

                                                grid_id = node.id.replace(/_grid_print/,'');
                                                grid = grids[grid_id];

                                                var store = stores[grid_id];
                                                var data = store.query({});

                                                if (data.length === 0) {
                                                        var no_data = document.createTextNode("None at this time");
                                                        node.appendChild(no_data);
                                                } else {
                                                        var table, tr, th, td, content, c;
                                                        var hasNumber = false;
                                                        table = document.createElement("table");
                                                        tr = document.createElement("tr");
                                                        for (c in grid.columns) {
                                                                content = document.createTextNode(grid.columns[c].label);
                                                                th = document.createElement("th");
                                                                th.appendChild(content);
                                                                tr.appendChild(th);
                                                                if (c === "number") hasNumber = true;
                                                        }
                                                        table.appendChild(tr);

                                                        if (hasNumber) {
                                                                data = store.query({},[{attribute:"number"}]);
                                                        }
                                                        var i = 1,j,l;
                                                        l = data.length;
                                                        for (j=0; j<l; j++) {
                                                                tr = document.createElement("tr");
                                                                for (c in grid.columns) {
                                                                        content = document.createTextNode(data[j][c]);
                                                                        td = document.createElement("td");
                                                                        td.appendChild(content);
                                                                        tr.appendChild(td);
                                                                }
                                                                table.appendChild(tr);
                                                                i++;
                                                        }
                                                        node.appendChild(table);
                                                }
                                        });
                                window.print()
                        };

The hasNumber code is used to order the rows by a number in one of the columns.

This post courtesy of Worktrainer.

HTML5 input tag required and placeholder shim for IE

Two of the nice HTML5 attributes for input tags are required and placeholder.

http://www.w3schools.com/html5/att_input_required.asp
http://www.w3schools.com/tags/att_input_placeholder.asp

Unfortunately, IE doesn’t support them. This code snippet allows a the required and placeholder attributes to act the same under IE as they do with browsers that support them. It also allows styling of disabled inputs and textareas under IE.

The only other code is some CSS that defines a class named “required” which is used to highlight the empty inputs.


.invalid {
        color: #800;
}
.required {
        border: 1px solid #800 !important;
}
.ie [placeholder] {
        color: #aaa;
}
.ie [placeholder]:focus {
        color: #000;
}
.ie [disabled]{
        color: #333;
        background-color: #efefef;
}


document.body.className += " ie";

if(!String.prototype.trim) {
        String.prototype.trim = function () {
                return this.replace(/^\s+|\s+$/,'');
        };
}

var form = document.querySelector('form');

if (form !== null) {
        var disabled_inputs = document.querySelectorAll("[disabled]");
        var required_inputs = document.querySelectorAll("[required]");
        var placeholders = document.querySelectorAll("[placeholder]");

        clearPlaceholder = function(evt) {
                var element;
                if (evt.target) {
                        evt.target.value = "";
                        element = document.getElementById(evt.target.id);
                } else if (evt.srcElement) {
                                evt.srcElement.value = "";
                                element = evt.srcElement;
                        } else {
                                return;
                        }
                element.className = element.className.replace(" required", "");
                if (element.removeEventListener) {
                        element.removeEventListener('focus', clearPlaceholder);
                } else if (element.detachEvent) {
                        element.detachEvent('onfocus', clearPlaceholder);
                }
        }

        var e;
        var l = placeholders.length;
        for (var p = 0; p < l; p++) {
                if (typeof placeholders[p].id !== undefined) {
                        e = document.getElementById(placeholders[p].id);
                        if (e !== null) {
                                e.value = e.getAttribute("placeholder");
                                if (e.addEventListener) {
                                        e.addEventListener('focus', clearPlaceholder);
                                } else if (e.attachEvent) {
                                        e.attachEvent('onfocus',clearPlaceholder);
                                }
                        }
                }
        }


        var e;
        var l = disabled_inputs.length;
        for (var d = 0; d < l; d++) {
                if (typeof disabled_inputs[d].id !== undefined) {
                        e = document.getElementById(disabled_inputs[d].id);
                        if (e !== null) {
                                e.removeAttribute("disabled");
                                e.className += " disabled";
                        }
                }
        }

        form.onsubmit = function() {
                var completed = true;
                var value = "";
                var inputs = document.querySelectorAll("input, textarea");
                var e, l = inputs.length;
                var req = /required/;
                var disabled = /disabled/;
                for (var i = 0; i < l; i++) {
                        e = document.getElementById(inputs[i].id);
                        if (e.hasAttribute("required")) {
                                if (!e.hasAttribute("placeholder")) {
                                        e.setAttribute("placeholder", "");
                                }
                                e.value = e.value.trim();
                                if ((e.value === "") || (e.value === e.getAttribute("placeholder"))) {
                                        e.value = e.getAttribute("placeholder");
                                        if (!req.test(e.className)) {
                                                e.className += " required";
                                                if (e.addEventListener) {
                                                        e.addEventListener('focus', clearPlaceholder);
                                                } else if (e.attachEvent) {
                                                        e.attachEvent('onfocus',clearPlaceholder);
                                                }
                                        }
                                        completed = false;
                                }
                        }
                        if (disabled.test(e.className)) {
                                e.setAttribute("disabled");
                                e.className = e.className.replace(" disabled", "");
                        }
                }
                return completed;
        }
}

Constructing targetted CSS

I am working on a page with a responsive design. The page is really two pages, assembled with PHP. This allows the content and functionality to be reduced for smaller mobile devices, and enhanced for tablets and desktops.

In order to provide consistent presentation across all devices, I broke the CSS into several files. All the CSS files are then compressed with the YUI compressor and concatenated into device specific files.

The device specific files are:

  • mobile.css - For devices that are not more than 360px wide
  • desktop_and_tablet.css - For devices that are more than 360px wide. Media queries indicate resolution specific elements.

for F in "$CSSDIR/"*.css; do
        # Compress
        echo -e "\t$F"
        `$YUICOMPRESSOR "$F" > "$TMPDIR/$F"`
done;

echo Concatenating CSS ...
# Construct targeted version
CSSTMPDIR="$TMPDIR/$CSSDIR"
cat "$CSSTMPDIR/reset.css" "$CSSTMPDIR/core.css" "$CSSTMPDIR/mobile.css" > "$MINDIR/mobile.css"
cat "$CSSTMPDIR/reset.css" "$CSSTMPDIR/core.css" "$CSSTMPDIR/color.css" "$CSSTMPDIR/desktop.css" "$CSSTMPDIR/tablet.css" "$CSSTMPDIR/js.css" "$CSSTMPDIR/common.css" > "$MINDIR/desktop_and_tablet.css"

The component CSS files are:

  • color.css - All color definitions
  • common.css - CSS that is common to both tablet and desktop devices
  • core.css - CSS that is common to all devices. Examples include setting the font, some common padding and margins.
  • desktop.css - Styles targeted for desktop browsers
  • js.css - Styles for pages which rely on JavaScript. These styles are applied if the body has the ‘js’ class applied, which indicates the client supports JavaScript.
  • mobile.css - The CSS for the smallest supported browsers
  • reset.css - The reset.css, thanks to: http://meyerweb.com/eric/tools/css/reset/
  • tablet.css - Styles for devices between 361px and 980px wide, inclusive

The advantages of this approach is that the style settings are not duplicated, they need only be maintained in one place. The content delivered to the mobile device includes only the styles and content which will be used, ensuring the leanest pages possible. The tablets and desktops, receive the CSS for tablets and desktops, but display the same content. This allows those pages to adjust dynamically to width changes.

All devices include a small JavaScript file which handles the page reload if the width of the device is modified outside the target bounds of the loaded page.

This is the JavaScript that detects and responds to the width change for mobile devices. It does require JavaScript, the page does not use media queries, since that would require more CSS and content to be loaded to support transitions.

function widthCheck() {
        if ((document.body.scrollWidth && document.body.scrollWidth > 360) ||
                (innerWidth && innerWidth > 360) ||
                (document.body.clientWidth && document.body.clientWidth > 360)) {
                        location.href = "index.php?notmobile";
        } 
}

window.onload = widthCheck;
window.onresize = widthCheck;

The ?notmobile overrides the mobile device detection code.

On the tablets and desktops, jQuery is used and the code to detect and react to width changes is:

        $(window).resize(function() {
                if ($(window).width() < 360) {
                        location.href='mobile.php';
                }
        });

PHP is used on initial page load to identify mobile devices therefore, mobile.php is used for devices which are classified as mobile, index.php is used for all others. A query string parameter is used to override the detection, which allows the viewer to switch between the pages to best suit their needs.