PHP Tutorial: Run Multiple Tic-Tac-Toe Game Instances (no database required)

Leave a comment Standard
Multi Instance Tic Tac Toe

Someone asked me about this in a comment recently so this post is specifically for you but I’m sure there are other people who will benefit from it as well. If you haven’t read the tic-tac-toe game tutorial yet then you’ll need to start there. This tutorial assumes you’ve already completed that and now you want to run two or more tic-tac-toe boards on the page simultaneously.

Or if you want to skip this tutorial you can
download the source code or try the working example

Concepts

The neat thing about classes is that you can use them to create multiple instances with varying states and data even though they all have the same methods and properties. Think about a monster RPG game. Each monster has a different name and breed and picture and strength — but they all have those things in common even though the values are different. Our tic tac toe class works the same way, we can create multiple instances of the game and they’ll each have different states — whose turn it is, which places on the board are filled — but they’ll all work the same way. In order to run multiple instances of our tic tac toe games we’re going to have to update our games so they can be uniquely identified by an instance number. The reason for this is so that when you play the game on instance 1 we know you want to update the state of instance 1 when you submit the form. If we didn’t give each game it’s own instance identifier then every time you made a move on one board it would reflect that move on all instances of the board at the same time.

Creating The Instance Form

Let’s change our index file so that we’re allowing the user to choose how many instances of the game they want to play. We start by removing where we created a new game from the top of the file. Since we want multiple instances we need to create the new game for every instance we have and since we don’t know how many instances we have at this point it needs to move down in our code base. Now once the user has selected how many instances of the game they want to play we need to dynamically generate that many. Using a for loop we loop through the number of instances they selected and create a game for each of those instances in our new $_SESSION[‘game’] array. Before this was only a single game, making it an array means we now have multiple games in our game session variable. Finally we create a new game instance and tell it to start playing. This is what our new index file looks like.

<?php
/***
* File: index.php
* Author: design1online.com, LLC
* Created: 4.6.2015
* License: Public GNU
* Description: PHP/MySQL Version of 2 Player Tic Tac Toe
* that allows playing multiple instances of the game at the same time
***/
require_once('oop/class.game.php');
require_once('oop/class.tictactoe.php');

//this will store their information as they refresh the page
session_start();

define('MAX_INSTANCES', 5); //the maximum number of games they can play at the same time

//trying to set number of instances to play
if (isset($_POST['instances'])) {
 if (is_numeric($_POST['instances']) && $_POST['instances'] > 0) {
 $_SESSION['instances'] = $_POST['instances'];
 }
}

?>
<html>
 <head>
 <title>Tic Tac Toe - Multiple Instances</title>
 <link rel="stylesheet" type="text/css" href="inc/style.css" />
 </head>
 <body>
 <div id="content">
 <form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="POST">
 <h2>Let's Play Tic Tac Toe!</h2>
 <?php
 //we need to know how many instances to create
 if (!isset($_SESSION['instances'])) {
 ?>
 <p>How many games would you like to instantiate?</p>
 <select name="instances">
 <?php
 for ($i = 1; $i <= MAX_INSTANCES; $i++) {
 echo "<option value=\"$i\">$i</option>";
 }
 ?>
 </select>
 <input type="submit" name="submit" value="Let Me Play!" />
 <?php
 } else {
 echo "<table width=\"100%\">
 <tr>";
 
 for ($i = 1; $i <= $_SESSION['instances']; $i++) {
 
 //if they haven't started a game yet let's load one
 if (!isset($_SESSION['game'][$i]['tictactoe'])) {
 $_SESSION['game'][$i]['tictactoe'] = new tictactoe($i);
 }
 
 echo "<td>";
 
 //play the game passing it the game data for that instance
 $_SESSION['game'][$i]['tictactoe']->playGame($_POST);
 
 echo "</td>";
 }
 
 echo "</tr>
 </table>";
 }
 ?>
 </form>
 </div>
 </body>
</html>

Adding Instances To The Class

The first thing we need to do is add an instance identifier to our tictactoe class. Since we need to know which game the player is trying to play adding a $this->instance value to our class will let us keep track of which game is which. Once that’s done we update our checks for $_POST data to make sure we’re using the correct data for the correct instance. Voila! Our games now only update when they see $_POST data that pertains to them. Our new tictactoe class file looks like this:

<?php
/***
* File: oop/class.tictactoe.php
* Author: design1online.com, LLC
* Created: 1.31.2012
* License: Public GNU
* Description: tic tac toe game
***/

class tictactoe extends game
{
 var $instance = 0; //the instance of this game
 var $player = "X"; //whose turn is
 var $board = array(); //the tic tac toe board
 var $totalMoves = 0; //how many moves have been made so far 

 /**
 * Purpose: default constructor
 * Preconditions: none
 * Postconditions: parent object started
 **/
 function tictactoe($instance)
 {
 /**
 * instantiate the parent game class so this class
 * inherits all of the game class's attributes 
 * and methods
 **/
 $this->instance = $instance;
 game::start();
 
 $this->newBoard();
 }
 
 /**
 * Purpose: start a new tic tac toe game
 * Preconditions: none
 * Postconditions: game is ready to be displayed
 **/
 function newGame()
 {
 //setup the game
 $this->start();
 
 //reset the player
 $this->player = "X";
 $this->totalMoves = 0;
 
 //reset the board
 $this->newBoard();
 }
 
 function newBoard() {
 
 //clear out the board
 $this->board = array();
 
 //create the board
 for ($x = 0; $x <= 2; $x++)
 {
 for ($y = 0; $y <= 2; $y++)
 {
 $this->board[$x][$y] = null;
 }
 }
 }
 
 /**
 * Purpose: run the game until it's tied or someone has won
 * Preconditions: all $_POST content for this game
 * Postconditions: game is in play
 **/
 function playGame($gamedata)
 {
 if (!$this->isOver() && isset($gamedata[$this->instance . 'move'])) {
 $this->move($gamedata);
 }
 
 //player pressed the button to start a new game
 if (isset($gamedata[$this->instance . 'newgame'])) {
 $this->newGame();
 }
 
 //display the game
 $this->displayGame();
 }
 
 /**
 * Purpose: display the game interface
 * Preconditions: none
 * Postconditions: start a game or keep playing the current game
 **/
 function displayGame()
 {
 
 //while the game isn't over
 if (!$this->isOver())
 {
 echo "<div id=\"board\">";
 
 for ($x = 0; $x < 3; $x++)
 {
 for ($y = 0; $y < 3; $y++)
 {
 echo "<div class=\"board_cell\">";
 
 //check to see if that position is already filled
 if ($this->board[$x][$y])
 echo "<img src=\"images/{$this->board[$x][$y]}.jpg\" alt=\"{$this->board[$x][$y]}\" title=\"{$this->board[$x][$y]}\" />";
 else
 {
 //let them choose to put an x or o there
 echo "<select name=\"{$this->instance}_{$x}_{$y}\">
 <option value=\"\"></option>
 <option value=\"{$this->player}\">{$this->player}</option>
 </select>";
 }
 
 echo "</div>";
 }
 
 echo "<div class=\"break\"></div>";
 }
 
 echo "
 <p align=\"center\">
 <input type=\"submit\" name=\"{$this->instance}move\" value=\"Take Turn\" /><br/>
 <b>It's player {$this->player}'s turn.</b></p>
 </div>";
 }
 else
 {
 
 //someone won the game or there was a tie
 if ($this->isOver() != "Tie")
 echo successMsg("Congratulations player " . $this->isOver() . ", you've won the game!");
 else if ($this->isOver() == "Tie")
 echo errorMsg("Whoops! Looks like you've had a tie game. Want to try again?");
 
 echo "<p align=\"center\"><input type=\"submit\" name=\"{$this->instance}newgame\" value=\"New Game\" /></p>";
 }
 }
 
 /**
 * Purpose: trying to place an X or O on the board
 * Preconditions: the position they want to make their move
 * Postconditions: the game data is updated
 **/
 function move($gamedata)
 { 

 if ($this->isOver())
 return;

 //remove duplicate entries on the board 
 $gamedata = array_unique($gamedata);
 
 foreach ($gamedata as $key => $value)
 {
 if ($value == $this->player)
 { 
 //update the board in that position with the player's X or O 
 $coords = explode("_", $key);
 
 //make sure we use the data from the right instance
 if ($coords[0] == $this->instance) {
 $this->board[$coords[1]][$coords[2]] = $this->player;

 //change the turn to the next player
 if ($this->player == "X")
 $this->player = "O";
 else
 $this->player = "X";
 
 $this->totalMoves++;
 }
 }
 }
 
 if ($this->isOver())
 return;
 }
 
 /**
 * Purpose: check for a winner
 * Preconditions: none
 * Postconditions: return the winner if found
 **/
 function isOver()
 {
 //top row
 if ($this->board[0][0] && $this->board[0][0] == $this->board[0][1] && $this->board[0][1] == $this->board[0][2])
 return $this->board[0][0];
 
 //middle row
 if ($this->board[1][0] && $this->board[1][0] == $this->board[1][1] && $this->board[1][1] == $this->board[1][2])
 return $this->board[1][0];
 
 //bottom row
 if ($this->board[2][0] && $this->board[2][0] == $this->board[2][1] && $this->board[2][1] == $this->board[2][2])
 return $this->board[2][0];
 
 //first column
 if ($this->board[0][0] && $this->board[0][0] == $this->board[1][0] && $this->board[1][0] == $this->board[2][0])
 return $this->board[0][0];
 
 //second column
 if ($this->board[0][1] && $this->board[0][1] == $this->board[1][1] && $this->board[1][1] == $this->board[2][1])
 return $this->board[0][1];
 
 //third column
 if ($this->board[0][2] && $this->board[0][2] == $this->board[1][2] && $this->board[1][2] == $this->board[2][2])
 return $this->board[0][2];
 
 //diagonal 1
 if ($this->board[0][0] && $this->board[0][0] == $this->board[1][1] && $this->board[1][1] == $this->board[2][2])
 return $this->board[0][0];
 
 //diagonal 2
 if ($this->board[0][2] && $this->board[0][2] == $this->board[1][1] && $this->board[1][1] == $this->board[2][0])
 return $this->board[0][2];
 
 if ($this->totalMoves >= 9)
 return "Tie";
 }
}

Conclusion

Try the working example! In this tutorial we talked about instances and how you can use a class to dynamically create multiple instances each of which keep track of their own state and values. If you’ve been following my game tutorials then I hope you’re starting to see how powerful classes are and how iterative improvements can be used to enhance your gameplay, functionality and user experience.

HTML5 Tutorial: Simple Music Player

Leave a comment Standard
HTML5 Music Player

So this is a pretty simple example but I was playing around with flash for my chat room and this was byproduct of that experimentation. It’s funny how even when I’m trying to learn something new I do my best to make it useful for other situations which is a good habit to get into if you’re not doing it already.

Try out the working demo or download the code

Caveats

This will only work on a browser that is HTML5 compliant and has support for audio. That means older browsers and most versions of Internet Explorer are not supported. Firefox, Chrome and Opera work as well as the newest versions of Internet Explorer.

The Audio Tag

If you’re familiar with HTML then most of the index file below will look pretty familiar. The thing you really have to pay attention to is the new <audio> tag. It looks like this:

<audio id="rock" src="http://whiteoakstables.net/chat/audio/rock.wav" preload="auto"></audio>

You may notice that it looks a whole lot like a hyperlink with the src that defines the location of the file. Preload is an optional attribute, it tells your browser to start downloading the file as soon as the DOM, or page elements, have finished loading. In addition to setting preload to true I’ve added an ID attribute so I can reference this particular song in my music player.

The Index File

<!DOCTYPE html>
<html>
 <head>
 <script data-require="jquery@*" data-semver="2.1.3" src="https://code.jquery.com/jquery-2.1.3.min.js"></script>
 <link data-require="bootstrap-glyphicons@*" data-semver="3.2.1" rel="stylesheet" href="http://netdna.bootstrapcdn.com/twitter-bootstrap/2.3.1/img/glyphicons-halflings.png" />
 <link data-require="bootstrap@*" data-semver="3.3.1" rel="stylesheet" href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.1/css/bootstrap.min.css" />
 <script data-require="bootstrap@*" data-semver="3.3.1" src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.1/js/bootstrap.min.js"></script>
 <link rel="stylesheet" href="style.css" />
 <script src="script.js"></script>
 </head>
 <body>
 <h1 class="text-center">Bootstrap HTML5 Music Player</h1>
 <audio id="rock" src="http://whiteoakstables.net/chat/audio/rock.wav" preload="auto"></audio>
 <audio id="blues" src="http://whiteoakstables.net/chat/audio/blues.wav" preload="auto"></audio>
 <audio id="electronica" src="http://whiteoakstables.net/chat/audio/electronica.wav" preload="auto"></audio>
 <audio id="classical" src="http://whiteoakstables.net/chat/audio/classical.wav" preload="auto"></audio>
 <audio id="latin" src="http://whiteoakstables.net/chat/audio/latin.wav" preload="auto"></audio>
 <audio id="indie" src="http://whiteoakstables.net/chat/audio/indie.wav" preload="auto"></audio>
 <div class="audiocontroller">
 <i class="audio-logo glyphicon glyphicon-headphones"></i>
 <i class="dynamiclink glyphicon glyphicon-play" id="play"></i></a>
 <i class="dynamiclink glyphicon glyphicon-pause" id="pause"></i>
 <select id="currentTrack">
 <option value="rock">Rock</option>
 <option value="blues">Blues</option>
 <option value="electronica">Electronica</option>
 <option value="classical">Classical</option>
 <option value="latin">Latin</option>
 <option value="indie">Indie</option>
 </select>
 <i class="dynamiclink glyphicon glyphicon-stop" id="stop"></i>
 </div>
 <p class="text-center">
 <a id="author" href="http://design1online.com" target="_blank">by Design1online.com, LLC</a>
 </p>
 </body>
</html>

The Javascript File

//Simple HTML5 Music Player by design1online.com, LLC

var currentTrack = null;
var paused = false;

$(document).ready(function() {
 currentTrack = $('#currentTrack').val();
 
 $("#play").click(function() { play(); });
 $("#stop").click(function() { stop(); });
 $("#pause").click(function() { pause(); });
});

function play() {

 //set the track back to the beginning
 if (!paused) {
 stop();
 currentTrack = $('#currentTrack').val();
 
 //make sure track starts from the begining each time
 document.getElementById(currentTrack).load();
 }

 //play the track
 document.getElementById(currentTrack).play();
}
 
//stop playing the current track
function stop() {
 if (currentTrack) {
 document.getElementById(currentTrack).pause();
 paused = false;
 }
}

//stop the track but don't go back to the beginning
function pause() {
 paused = true;
 document.getElementById(currentTrack).pause();
}

How It All Works

So now let’s talk about the javascript file. At first glance it looks relatively simple because it is! When you want to select a track you simply ask for it using getElementById and then you tell it to play or to pause. One thing that is sadly lacking is a stop() functionality. With html5 you can only pause an audio file — which does stop the playback but it also keeps the audio file at the same position as when you called pause on it. This means that if you wanted to play the audio file from the beginning again and you just called play() it would continue playing from where it left off. That’s where the load() function comes into play — this will tell the audio track to start loading if it wasn’t set to preload and it will reset the track back to the beginning.

The only other thing to mention is the currentTrack variable. I use this to keep track of which of the songs is currently selected and the user wasn’t to listen to.

Google Maps Tutorial: You Are Here in 3 Easy Steps

Comments 2 Standard
google maps

I’ve been playing with Google maps lately and I thought I would share this little tidbit. It still needs some refinement (ie fetching your location from IP for non-html5 compliant browsers and letting you set your location) but it may give you the start you were looking for.

1. Create a Google maps API key

You can find the instructions on the Google maps Getting Started documentation. Basically you need to generate the key for the website that will be hosting this code so that Google will honor the API request.

2. Create a PHP File

<!DOCTYPE html>
<html>
  <head>
    <meta name="viewport" content="initial-scale=1.0, user-scalable=no">
    <meta charset="utf-8">
    <title>You Are Here</title>
    <style>
      html, body {
        height: 100%;
        margin: 0px;
        padding: 0px
      }
            
      #map-canvas {
        float: right;
        height: 100%;
        margin: 0px;
        padding: 0px
      }
    </style>
    <script src="https://maps.googleapis.com/maps/api/js?v=3.exp&sensor=false"></script>
        <script src="javascript.js"></script>
  </head>
  <body>
    <div id="map-canvas" />
  </body>
</html>

3. Create a Javascript File

I called mine javascript.js. If you change the name you’ll need to update it in your .php file above.

/**
* Purpose: ask the browser for the user's location (html5)
* Preconditions: html5 is enabled on the browser
* Postconditions: map initialized with user's position if browser is html5 enabled and position given
**/
function getLocation()
{
    if (navigator.geolocation)
        navigator.geolocation.getCurrentPosition(initMap);
    else
        alert("Geolocation is not supported by this browser.");
}

/**
* Purpose: start the google map
* Preconditions: position object
* Postconditions: map centered around the user's position with a zoom of 15
**/
function initMap(position) {

    console.log('Your position is:', position.coords.latitude, position.coords.longitude);
    
  var mapOptions = {
    zoom: 15,
    center: new google.maps.LatLng(position.coords.latitude, position.coords.longitude)
  };

  var map = new google.maps.Map(document.getElementById('map-canvas'), mapOptions);
    var position = new google.maps.LatLng(position.coords.latitude, position.coords.longitude);
 
    setMarker(map, position, "You!", "You Are Here");
}

/**
* Purpose: set a marker on a map
* Preconditions: map reference, position, title of the marker window, message in the marker window
* Postconditions: marker event listener added to the map
**/
function setMarker(map, position, title, message) {

     var marker = new google.maps.Marker({
      position: position,
      map: map
    });
        
  marker.setTitle(title);
        
  var infowindow = new google.maps.InfoWindow({
    content: message
  });

  google.maps.event.addListener(marker, 'click', function() {
    infowindow.open(marker.get('map'), marker);
  });
}

google.maps.event.addDomListener(window, 'load', getLocation);

Demo Working Code

Want to see what it does? Fork this plunker to try it out and see the code. Remember you have to be using a HTML5 compliant browser and give it permission to view your location.

Advanced Angular Chatroom Tutorial: No Database or Sockets Required

Leave a comment Standard
angular chat with pubnub

This is an expansion on my basic angular chatroom to add new functionality. In this version of the chat room there is now a popup color picker to change font colors, a way to change from one channel to another, click-able hyperlinks, HTML messages and emoticons.

1. Create a PubNub Account

To get started you will need to create a PubNub account. This will handle all the chat messages for you so you don’t need a server or database in order to quickly and easily create your chat room. There is a free plan and several paid for plans available for you to choose from. Once your account is created you’ll need to find your subscribe key and publish key.

2. Create the chat file

Now you’re going to create the main chat file. This will display your chat room. At the top of the file replace the subscribe_key and publish_key with the one you see in your newly created account.

index.html

<!doctype html>
<html ng-app="angular_chat">
  <head>
    <script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.0.5/angular.js"></script>
    <script src="http://code.jquery.com/jquery-1.9.1.js"></script>
    <script src="http://angular-ui.github.io/bootstrap/ui-bootstrap-tpls-0.6.0.js"></script>
    <div pub-key="PUB_KEY_GOES_HERE" sub-key="SUB_KEY_GOES_HERE" ssl="off" origin="pubsub.pubnub.com" id="pubnub"></div>
    <script src="http://cdn.pubnub.com/pubnub-3.1.min.js"></script>
    <script src="angular-chat.js"></script>
    <script src="angular-colorpicker.js"></script>
    <script src="angular-colorpicker-modal.js"></script>
    <link rel="stylesheet" href="style.css" rel="stylesheet">
    <link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.1/css/bootstrap-combined.min.css" rel="stylesheet">
  </head>
  <body>
  <div class='container-fluid' ng-controller="chatCtrl">
    <div ng-include="'chatHeader.html'"></div>
    <div ng-Show="!loggedIn" id="login">
      <h2>Login</h2>
      <label for="username">
        <b>Username:</b>
      </label>
      <label>
        <input type="text" ng-model="message.username" />
      </label>
      <span ng-click="attemptLogin()">
        <i></i> Go Chat
      </span>
    </div>
    <div ng-Show="loggedIn" id="chat">
      <table>
        <tr ng-repeat="chat in chatMessages">
            <td style="width: 30%;">
              <b>{{chat.username}}</b><br/>
              <small>
               {{chat.date}} @ {{chat.time}}
              </small>
            </td>
            <td colspan="2">
              <font color="{{chat.color}}"><span ng-bind-html-unsafe="chat.text"></span></font>
            </td>
        </tr>
        <tr ng-show="chatMessages.length == 0">
          <td colspan="3">No messages yet!</td>
        </tr>
      </table>
    </div>
    <form ng-Show="loggedIn" ui-keypress="{13:'postMessage()'}">
      <div id="inputMessage">
        <div ng-controller="ColorPickerCtrl">
          <script type='text/ng-template' id='fontcolor.html'>
            <div>
              <h3>Change Font Color</h3>
            </div>
            <div ng-modal="colorPicker">
              <ng-color-circle height="250" width="250" model-object="colorpicker" model-property="color"></ng-color-circle>
              <div style="background-color:{{colorpicker.color}}">Selected Color</div>
            </div>
            <div>
              <button ng-click="ok()">Done</button>
            </div>
          </script>
          <span ng-click="open()" id="fontModalButton" style="background:{{color}} !important;">
            <i></i>
          </span>
        </div>
        <span ng-click="toggleEmoticons = !toggleEmoticons">
          <img ng-src="{{emoticon_url}}{{emoticons[':)']}}" />
        </span>
        <input type="text" placeholder="Say hello!" ng-model="message.text" />
        <span ng-click="postMessage()">
          <i></i> Post
        </span>
      </div>
      <span ng-show="toggleEmoticons == true" id="emoticonList" ng-repeat="(key, image) in emoticons">
        <img ng-src="{{emoticon_url}}{{image}}" ng-click="insertEmoticon(key)" />
      </span>
    </form>
    <div ng-include="'chatFooter.html'"></div>
  </div>
  </body>
</html>

3. Create the header and footer files

chatHeader.html

<div>
  <h1>Ng-Angular Chat</h1>
  <h5><i></i> using Bootstrap and PubNub</h5>
  <p>&nbsp;</p>
  <div ng-show="realtimeStatus == 0">
    <span><i></i></span>
  </div>
  <div ng-show="realtimeStatus == 1">
    <span><i></i></span>
  </div>
  <div ng-show="realtimeStatus == 2">
    <span><i></i></span>
  </div>
  <div id="channelDropdown"><select ng-model="selectedChannel" ng-change="initChat(selectedChannel)" ng-options="obj.value as obj.text for obj in chatChannels"></select></div>
  <div ng-show="loggedIn" id="logout">
    <span ng-click="attemptLogout()"><i></i> Logout</span>
  </div>
</div>
<p>&nbsp;</p>
<div ng-show="errorMsg">
  <i></i> <b>Error:</b> {{errorMsg}}
</div>

chatFooter.html

<div>
  2013 &copy; design1online.com
</div>

4. Create the stylesheet

This example is using bootstrap however we still need some additional tweaks to get it looks just right.

style.css

#chat {
 height: 31em; 
 overflow: auto;
 background-color: #EEE;
 border-radius: .25em;
 margin-bottom: 1em;
}

#logout {
  float: right;
  margin-top: -1.35em;
}

#channelDropdown {
  margin-right: .3em;
  margin-left: .3em;
  float: left;
  margin-top: -1.35em;
}

#fontModalButton {
  height: 21px;
}

#selectedColor {
  width: 100%;
  border: 1px solid #000000; 
}

.alert {
  padding: 8px !important;
}

.status {
  float: left;
  margin-top: -1em;
}

.left {
  float: left;
  margin-right: .25em;
}

.icon {
  cursor: pointer;
}

5. Create the angular files

angular-chat.js

/***
 * File: angular-chat.js
 * Author: Jade Krafsig
 * Source: design1online.com, LLC
 * License: GNU Public License
 ***/

/***
 * Purpose: load bootstrap ui angular modules
 * Precondition: none
 * Postcondition: modules loaded
 ***/
angular.module('angular_chat', ['ui.bootstrap', 'directive.colorPicker']);

/***
 * Purpose: load the existing chat logs
 * Precondition: none
 * Postcondition: chat logs have been loaded
 ***/
function chatCtrl($scope, $http) { 

  /***
   * Configurable global variables
   ***/
  $scope.messageLimit = 50;
  $scope.defaultUsername = "Guest";
  $scope.selectedColor = "#000000";

  $scope.chatChannels = [
    {
      text: "Basic Channel",
      value: "angular_chat", 
      default: true
    },
    {
      text: "Advanced Channel",
      value: "angular_chat_advanced"
    },
  ];

  $scope.emoticon_url = "http://www.freesmileys.org/smileys/smiley-basic/";
  $scope.emoticons = {
    ':-)' : 'biggrin.gif',
    ':)' : 'biggrin.gif',
    ':D' : 'laugh.gif',
    ':-D' : 'laugh.gif',
    ':-|' : 'mellow.gif',
    ':|' : 'mellow.gif',
    ':-p' : 'tongue.gif',
    ':p' : 'tongue.gif',
    ':-(' : 'sad.gif',
    ':(' : 'sad.gif'
  };

  /***
   * Static global variables, do not change
   ***/
  $scope.loggedIn = false;
  $scope.errorMsg;
  $scope.realtimeStatus = 0;
  $scope.toggleEmoticons = false;

  /***
   * Purpose: clear the message object
   * Precondition: none
   * Postcondition: message object has been reset
   ***/
  $scope.clearMsg = function() {
    $scope.message = {
      username: $scope.defaultUsername,
      email: 'n/a',
      text: ''
    };
  }

  /***
   * Purpose: listen for a font color change broadcast
   * Precondition: color value
   * Postcondition: selected color has been set
   ***/
  $scope.$on('colorChange', function(obj, color){
    $scope.selectedColor = color;
  });    

  /***
   * Purpose: load the existing chat logs
   * Precondition: none
   * Postcondition: chat logs have been loaded
   ***/
  $scope.chatLogs = function() {
    PUBNUB.history( {
      channel : $scope.selectedChannel,
      limit   : $scope.messageLimit
    }, function(messages) {
      // Shows All Messages
      $scope.$apply(function(){
        $scope.chatMessages = messages.reverse();          
      }); 
    });
   }

  /***
   * Purpose: load the existing chat logs
   * Precondition: none
   * Postcondition: chat logs have been loaded
   ***/
   $scope.attemptLogin = function() {
    $scope.errorMsg = "";

    if (!$scope.message.username) {
      $scope.errorMsg = "You must enter a username.";
      return;
    }

    if (!$scope.realtimeStatus) {
      $scope.errorMsg = "You're not connect to PubNub.";
      return;
    }

    $scope.loggedIn = true;
   }

  /***
   * Purpose: remove error message formatting when the message input changes
   * Precondition: none
   * Postcondition: error message class removed from message input
   ***/
  $scope.$watch('message.text', function(newValue, oldValue) {
    if (newValue) {
      $("#inputMessage").removeClass("error");
      $scope.errorMsg = "";
    }
  }, true);

  /***
   * Purpose: trying to post a message to the chat
   * Precondition: loggedIn
   * Postcondition: message added to chatMessages and sent to chatLog
   ***/
  $scope.postMessage = function() {

    //make sure they are logged in
    if (!$scope.loggedIn) {
      $scope.errorMsg = "You must login first.";
      return;
    }

    //make sure they enter a chat message
    if (!$scope.message.text) {
      $scope.errorMsg = "You must enter a message.";
      $("#inputMessage").addClass("error");
      return;
    }

    //set the message date
    d = new Date();
    $scope.message.date = d.getDay() + "/" + d.getMonth() + "/" + d.getFullYear();
    $scope.message.time = d.getHours() + ":" + d.getMinutes() + ":" + d.getSeconds();

    //change font color
    $scope.message.color = $scope.selectedColor;

    //replace hyperlinks with clickable urls
    $scope.message.text = $scope.replaceURLWithLink($scope.message.text);

    //replace smileys
    $scope.message.text = $scope.replaceEmoticons($scope.message.text);

    //send the message to the selected channel
    PUBNUB.publish({
      channel : $scope.selectedChannel,
      message : $scope.message
    });

    //reset the message input box
    $scope.message.text = "";
  };

  /***
   * Purpose: connect and access pubnub channel
   * Preconditions: pubnub js file init
   * Postconditions: pubnub is waiting and ready
   ***/
  $scope.initChat = function(newChannel) {
    if (newChannel)
      $scope.selectedChannel = newChannel;

    if (!$scope.chatChannels.length) {
      $scope.errorMsg = "Missing chat channels, check config.";
      return;
    }

    if (!$scope.selectedChannel) {
      $scope.errorMsg = "You must select a channel.";
      return;
    }

    //clear out any chat messages from a previous channel
    $scope.chatMessages = Array();

    //unsubscribe to the active channel
    angular.forEach($scope.chatChannels, function(channel, key){
      PUBNUB.unsubscribe({channel: channel.value});
    });

    //subscribe to the selected channel
    PUBNUB.subscribe({
      channel    : $scope.selectedChannel,
      restore    : false, 
      callback   : function(message) { 
          //update messages with the new message
          $scope.$apply(function(){
          $scope.chatMessages.unshift(message);          
        }); 
      },

      error      : function(data) {
        $scope.errorMsg = data;
      },

      disconnect : function() {   
        $scope.$apply(function(){
          $scope.realtimeStatus = 0;
        });
      },

      reconnect  : function() {   
        $scope.$apply(function(){
          $scope.realtimeStatus = 1;
        });
      },

      connect    : function() {
        $scope.$apply(function(){
          $scope.realtimeStatus = 2;
          //load the chat logs
          $scope.chatLogs();
        });
      }
    });
  }

  /***
   * Purpose: trying to post a message to the chat
   * Precondition: loggedIn
   * Postcondition: message added to chatMessages and sent to chatLog
   ***/
  $scope.attemptLogout = function() {
    $("#inputMessage").removeClass("error");
    $scope.clearMsg();
    $scope.loggedIn = false;
  }

  /***
   * Purpose: set the chat channel to the default channel value
   * Precondition: at least one channel defined
   * Postcondition: selectedChannel set to the default
   ***/
  $scope.defaultChannel = function() {
    var chatChannel;

    angular.forEach($scope.chatChannels, function(channel, key){

     if (!chatChannel)
      chatChannel = channel;

     if (channel.default) {
      chatChannel = channel;
      return;
     }
    });

    $scope.selectedChannel = chatChannel.value;
  }

  /***
   * Purpose: add an emoticon to the message input
   * Precondition: emoticon has been selected
   * Postcondition: emoticon value appended to message input
   ***/
  $scope.insertEmoticon = function(selected) {
    $scope.message.text = $scope.message.text + " " + selected + " ";
  }

  /***
   * Purpose: regex to replace emoticons with image html
   * Precondition: text to replace emoticons in
   * Postcondition: any emoticons found have been replaced with html images
   ***/
  $scope.replaceEmoticons = function(text) {
    patterns = [];
    metachars = /[[\]{}()*+?.\\|^$\-,&#\s]/g;

    // build a regex pattern for each defined property
    for (var i in $scope.emoticons) {
      if ($scope.emoticons.hasOwnProperty(i)){ // escape metacharacters
        patterns.push('('+i.replace(metachars, "\\$&")+')');
      }
    }

    // build the regular expression and replace
    return text.replace(new RegExp(patterns.join('|'),'g'), function (match) {
      return typeof $scope.emoticons[match] != 'undefined' ?
        '<img src="'+$scope.emoticon_url+$scope.emoticons[match]+'"/>' :
        match;
    });
  }

  /***
   * Purpose: replace url text with a clickable link (opens in a new window)
   * Precondition: text to replace urls in
   * Postcondition: urls have been replaced with a clickable hyperlink
   ***/
  $scope.replaceURLWithLink = function(text) {
    var exp = /(\b(https?):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])/ig;
    return text.replace(exp,"<a href='$1' target='_blank'>$1</a>"); 
  }

  /***
   * Purpose: Initialize the chatroom
   * Preconditions: none
   * Postconditions: none
   ***/
  $scope.clearMsg();
  $scope.defaultChannel();
  $scope.initChat();

}

angular-colorpicker-modal.js

/***
 * File: angular-colorpicker-modal.js
 * Author: Jade Krafsig
 * Source: design1online.com, LLC
 * License: GNU Public License
 ***/

/***
 * Purpose: modal template controller
 * Preconditions: $rootScope, $modal and ui-bootstrap module
 * Postconditions: creates a modal window using the specified template
 *  and broadcasts the selected color back to the main controller when
 *  the window is closed
 ***/
var ColorPickerCtrl = function ($rootScope, $scope, $modal) {

  $scope.open = function () {
    $scope.color = "#000000";

    var modalInstance = $modal.open({
      templateUrl: 'fontcolor.html',
      controller: 'ColorpickerModalCtrl'
    });

    //when the color changes broadcast back to the main chat controller
    modalInstance.result.then(function (data) {
      $scope.color = data.color;
      $rootScope.$broadcast('colorChange',  $scope.color);
    });
  }
}

//create a modal popup window and pass it back to the modal parent controller
/***
 * Purpose: modal controller
 * Preconditions: instance of the modal being created
 * Postconditions: functionality defined for all modals created of $modalInstance
 ***/
var ColorpickerModalCtrl = function ($scope, $modalInstance) {
  $scope.colorpicker = {};
  $scope.colorpicker.color = "#000000";

  $scope.ok = function () {
    $modalInstance.close({color: $scope.colorpicker.color});
  };
};

angular-colorpicker.js

/***
 * Method taken from Brian Grinstead and modified the return 
 * of rounded r,g and b.
 * @https://github.com/bgrins/TinyColor/blob/master/tinycolor.js
 ***/
(function(angular) {
  var ngColorPicker = angular.module('directive.colorPicker', []);

  function hsvToRgb(h, s, v) {
    h*=6;
    var i = ~~h,
      f = h - i,
      p = v * (1 - s),
      q = v * (1 - f * s),
      t = v * (1 - (1 - f) * s),
      mod = i % 6,
      r = [v, q, p, p, t, v][mod] * 255,
      g = [t, v, v, q, p, p][mod] * 255,
      b = [p, p, t, v, v, q][mod] * 255;

      return [~~r, ~~g, ~~b, "rgb("+ ~~r + "," + ~~g + "," + ~~b + ")"];
  }

  function rgbToHex(r, g, b) {
    return "#" + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
  }

  ngColorPicker.directive('ngColorCircle', ['$timeout', function($timeout) {
    return {
      restrict  : 'E',
      replace   : true,
      scope     : '@=',
      template  : '<div>'
      +             '<canvas id="colorCircle">'
      +             '</canvas>'
      +           '</div>',
      compile: function compile(tElement, tAttrs, transclude) {
        return {
          pre: function preLink(scope, iElement, iAttrs, controller) {
            var canvas        = document.getElementById("colorCircle"),
                context       = canvas.getContext('2d'),
                width         = canvas.width = iAttrs.width ||  300,
                height        = canvas.height = iAttrs.height || 300,
                imageData     = context.createImageData(width, height),
                pixels        = imageData.data,
                oneHundred    = 100,
                two55         = 255,
                circleOffset  = 10,
                diameter      = width - circleOffset * 2,
                wheelPixel    = circleOffset * 4 * width + circleOffset * 4;

            iElement.css({
              width: width,
              height: height
            });

            scope.radius            = diameter / 2;
            scope.radiusPlusOffset  = scope.radius + circleOffset;
            scope.radiusSquared     = scope.radius * scope.radius;
            scope.currentY          = oneHundred;
            scope.currentX          = -scope.currentY;

            for (y = 0; y < height; y++) {
              for (x = 0; x < width; x++) {
                var rx  = x - scope.radius,
                    ry  = y - scope.radius,
                    d   = rx * rx + ry * ry,
                    rgb = hsvToRgb((Math.atan2(ry, rx) + Math.PI) / (Math.PI * 2),
                          Math.sqrt(d) / scope.radius, 1); 

                pixels[wheelPixel++] = rgb[0];
                pixels[wheelPixel++] = rgb[1];
                pixels[wheelPixel++] = rgb[2];
                pixels[wheelPixel++] = d > scope.radiusSquared ? 0 : two55;
              }
            }

            context.putImageData(imageData, 0, 0);
          },
          post: function postLink(scope, iElement, iAttrs, controller) { 
            $(iElement).click(function(event) {
              scope.currentX = event.pageX - this.offsetLeft - scope.radiusPlusOffset || scope.currentX;
              scope.currentY = event.pageY - this.offsetTop - scope.radiusPlusOffset || scope.currentY;

              var theta = Math.atan2(scope.currentY, scope.currentX),
                  d = scope.currentX * scope.currentX + scope.currentY * scope.currentY;

              if (d > scope.radiusSquared) {
                scope.currentX = scope.radius * Math.cos(theta);
                scope.currentY = scope.radius * Math.sin(theta);
                theta = Math.atan2(scope.currentY, scope.currentX);
                d = scope.currentX * scope.currentX + scope.currentY * scope.currentY;
              }

              var color = hsvToRgb((theta + Math.PI) / (Math.PI * 2),
                Math.sqrt(d) / scope.radius, 1);
              var hex = rgbToHex(color[0], color[1], color[2]);

              $timeout(function(){
                scope.$parent[iAttrs.modelObject][iAttrs.modelProperty] = hex;                
              });
            });
          }
        };
      }
    };
  }]);

  ngColorPicker.directive('ngColorPicker', [function() {
    return {
      restrict: 'E',
      replace: true,
      scope : '@=',
      template: '<table>'
      +           '<tr>'
      +           '<td ng-repeat="color in colorList">'
      +             '<div style="width: 8px; height: 8px; border: {{color.select}}px solid #000; padding: 5px; background-color: {{color.color}}" ng-click="selectColor(color)">'
      +             '</div>'
      +           '<td>'
      +           '</tr>'
      +         '</table>',
      compile: function compile(tElement, tAttrs, transclude) {
        return {
          post: function postLink(scope, iElement, iAttrs, controller) { 
            scope.modelObject   = iAttrs.modelObject;
            scope.modelProperty = iAttrs.modelProperty;
            scope.colorList = [];
            angular.forEach(colors, function(color) {
              scope.colorList.push({
                color : color,
                select : 1
              });
            });
          }
        };
      },
      controller: function($scope, $element, $timeout) {
        $scope.selectColor = function(color) {
          for (var i = 0; i < $scope.colorList.length; i++) {
            $scope.colorList[i].select = 1;
            if ($scope.colorList[i] === color) {
              $scope[$scope.modelObject][$scope.modelProperty] = color.color;
              $scope.colorList[i].select = 2;
            }
          }
        };
      }
    };
  }]);
})(window.angular);

6. Working Demo

That’s it. Now you can test your files or you can be lazy and fork my working version on plunker or fork it on GitHub.

AngularJS Chatroom Tutorial: No Database or Sockets Required

Comments 8 Standard
angular chat with pubnub

So I’ve been playing around with Angular lately and I decide to try my hand at making an Angular chat room since angular is great when it comes to keeping the page display current as the DOM changes. In order to deal with the message sending (and for the ease of getting a working version going) I decided to use PubNub which handles all the message transport quickly and easily.

1. Create A PubNub Account

In order to use this you’ll need to sign up for a PubNub account. There is a free plan and several paid for plans available for you to choose from. Once your account is created you’ll need to find your subscribe key and publish key.

2. Create the chat file

Now you’re going to create the main chat file. This will display your chat room. At the top of the file replace the subscribe_key and publish_key with the one you see in your newly created account.

index.html

<!doctype html>
<html ng-app="angular_chat">
  <head>
    <script src="http://ajax.googleapis.com/ajax/libs/angularjs/1.0.5/angular.js"></script>
    <script src="http://code.jquery.com/jquery-1.9.1.js"></script>
    <script src="http://angular-ui.github.io/bootstrap/ui-bootstrap-tpls-0.4.0.js"></script>
    <div pub-key="PUB_KEY_GOES_IN_HERE" sub-key="SUBSCRIBE_KEY_GOES_IN_HERE" ssl="off" origin="pubsub.pubnub.com" id="pubnub"></div>
    <script src="http://cdn.pubnub.com/pubnub-3.1.min.js"></script>
    <script src="angular-chat.js"></script>
    <link rel="stylesheet" href="style.css" rel="stylesheet">
    <link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.1/css/bootstrap-combined.min.css" rel="stylesheet">
  </head>
  <body>
  <div class='container-fluid' ng-controller="chatCtrl">
    <div ng-include="'chatHeader.html'"></div>
    <div ng-Show="!loggedIn" id="login">
      <h2>Login</h2>
      <label for="username">
        <b>Username:</b>
      </label>
      <label>
        <input type="text" ng-model="message.username" />
      </label>
      <span ng-click="attemptLogin()">
        <i></i> Go Chat
      </span>
    </div>
    <div ng-Show="loggedIn" id="chat">
      <table>
        <tr ng-repeat="chat in chatMessages">
            <td style="width: 30%;">
              <b>{{chat.username}}</b><br/>
              <small>
               {{chat.date}} @ {{chat.time}}
              </small>
            </td>
            <td colspan="2">{{chat.text}}</td>
        </tr>
        <tr ng-show="chatMessages.length == 0">
          <td colspan="3">No messages yet!</td>
        </tr>
      </table>
    </div>
    <form ng-Show="loggedIn" ui-keypress="{13:'postMessage()'}">
      <div id="inputMessage">
        <input type="text" placeholder="Say hello!" ng-model="message.text" />
        <span ng-click="postMessage()">
          <i></i> Post
        </span>
      </div>
    </form>
    <div ng-include="'chatFooter.html'"></div>
  </div>
  </body>
</html>

3. Create the angular file

Now we’re going to add in a file for our angular scripts. At the top of the file you’ll find some config variables which you can change as you see fit to set the default username, how many messages to show at a time, and the name of the chat channel you’ll be connecting to.

angular-chat.js

/***
 * File: angular-chat.js
 * Author: Jade Krafsig
 * Source: design1online.com, LLC
 * License: GNU Public License
 ***/

/***
 * Purpose: load bootstrap ui angular modules
 * Precondition: none
 * Postcondition: modules loaded
 ***/
angular.module('angular_chat', ['ui.bootstrap']);

/***
 * Purpose: load the existing chat logs
 * Precondition: none
 * Postcondition: chat logs have been loaded
 ***/
function chatCtrl($scope, $http) { 

  /***
   * Configurable global variables
   ***/
  $scope.chatChannel = "angular_chat";
  $scope.messageLimit = 50;
  $scope.defaultUsername = "Guest";

  /***
   * Static global variables
   ***/
  $scope.loggedIn = false;
  $scope.errorMsg;
  $scope.realtimeStatus = 0;

  /***
   * Purpose: clear the message object
   * Precondition: none
   * Postcondition: message object has been reset
   ***/
  $scope.clearMsg = function() {
    $scope.message = {
      username: $scope.defaultUsername,
      email: 'n/a',
      text: ''
    };
  }

  $scope.clearMsg();

  /***
   * Purpose: load the existing chat logs
   * Precondition: none
   * Postcondition: chat logs have been loaded
   ***/
  $scope.chatLogs = function() {
    PUBNUB.history( {
      channel : $scope.chatChannel,
      limit   : $scope.messageLimit
    }, function(messages) {
      // Shows All Messages
      $scope.$apply(function(){
        $scope.chatMessages = messages.reverse();          
      }); 
    });
   }

  /***
   * Purpose: load the existing chat logs
   * Precondition: none
   * Postcondition: chat logs have been loaded
   ***/
   $scope.attemptLogin = function() {
    $scope.errorMsg = "";

    if (!$scope.message.username) {
      $scope.errorMsg = "You must enter a username.";
      return;
    }

    if (!$scope.realtimeStatus) {
      $scope.errorMsg = "You're not connect to PubNub.";
      return;
    }

    $scope.loggedIn = true;
   }

  /***
   * Purpose: remove error message formatting when the message input changes
   * Precondition: none
   * Postcondition: error message class removed from message input
   ***/
  $scope.$watch('message.text', function(newValue, oldValue) {
    if (newValue)
      $("#inputMessage").removeClass("error");
      $scope.errorMsg = "";
  }, true);

  /***
   * Purpose: trying to post a message to the chat
   * Precondition: loggedIn
   * Postcondition: message added to chatMessages and sent to chatLog
   ***/
  $scope.postMessage = function() {

    //make sure they are logged in
    if (!$scope.loggedIn) {
      $scope.errorMsg = "You must login first.";
      return;
    }

    //make sure they enter a chat message
    if (!$scope.message.text) {
      $scope.errorMsg = "You must enter a message.";
      $("#inputMessage").addClass("error");
      return;
    }

    //set the message date
    d = new Date();
    $scope.message.date = d.getDay() + "/" + d.getMonth() + "/" + d.getFullYear();
    $scope.message.time = d.getHours() + ":" + d.getMinutes() + ":" + d.getSeconds();

   PUBNUB.publish({
      channel : $scope.chatChannel,
      message : $scope.message
    });

    $scope.message.text = "";
  };

  /***
   * Purpose: connect and access pubnub channel
   * Preconditions: pubnub js file init
   * Postconditions: pubnub is waiting and ready
   ***/
  PUBNUB.subscribe({
    channel    : $scope.chatChannel,
    restore    : false, 
    callback   : function(message) { 
      //update messages with the new message
      $scope.$apply(function(){
        $scope.chatMessages.unshift(message);          
      }); 
    },

    error      : function(data) {
      $scope.errorMsg = data;
    },

    disconnect : function() {   
      $scope.$apply(function(){
        $scope.realtimeStatus = 0;
      });
    },

    reconnect  : function() {   
      $scope.$apply(function(){
        $scope.realtimeStatus = 1;
      });
    },

    connect    : function() {
      $scope.$apply(function(){
        $scope.realtimeStatus = 2;
        //load the chat logs
        $scope.chatLogs();
      });
    }
  });

  /***
   * Purpose: trying to post a message to the chat
   * Precondition: loggedIn
   * Postcondition: message added to chatMessages and sent to chatLog
   ***/
  $scope.attemptLogout = function() {
    $("#inputMessage").removeClass("error");
    $scope.clearMsg();
    $scope.loggedIn = false;
  }
}

4. Create the header and footer files

These files will put a static header and footer on your chat room.

chatHeader.html

<div>
  <h1>Angular Chat</h1>
  <h5><i></i> using Bootstrap and PubNub</h5>
  <p>&nbsp;</p>
  <div ng-show="realtimeStatus == 0">
    <span><i></i> Disconnected</span>
  </div>
  <div ng-show="realtimeStatus == 1">
    <span><i></i> Connecting...</span>
  </div>
  <div ng-show="realtimeStatus == 2">
    <span><i></i> Connected</span>
  </div>
  <div ng-show="loggedIn" id="logout">
    <span ng-click="attemptLogout()"><i></i> Logout</span>
  </div>
</div>
<p>&nbsp;</p>
<div ng-show="errorMsg">
  <i></i> <b>Error:</b> {{errorMsg}}
</div>

chatFooter.html

<div>
  2013 &copy; design1online.com
</div>

5. Create the stylesheet

This will adjust the height of the chat window and the chat header status and logout buttons.

style.css

#chat {
 height: 33em; 
 overflow: auto;
 background-color: #EEE;
 border-radius: .25em;
 margin-bottom: 1em;
}

.status {
  float: left;
  margin-top: -1em;
}

#logout {
  float: right;
  margin-top: -1.2em;
}

6. Try the working version

See what the chat room looks like on my plunkr account or fork it on GitHub.

Angular Js

Leave a comment Standard
AngularJS

I wrote a few plunkers this past month that I thought I would share for anyone whose interested in dabbling in Angular JS. Enjoy!

Javascript: Charts, Graphs and Data Visualization for the Web

Comments 2 Standard
file00010254296302

I was searching for some graphing, charting and data visualization libraries and I thought it might be helpful to share my finds and my favorite. Enjoy!

  • High Charts – Best looking, easiest to use and by far has the most features. The downside… it’s also the most expensive. A developer license will run you around $400 and an unlimited developer license is $3,600. They also have a stock ticker version. Nice stuff and my first choice if you can afford it.
  • jqPlot – is the best freeware, open source library out there in my opinion. It allows you to customize the colors and axis labels and has support for bar, scatter, bubble, pie and area graphs. It’s not as easy to use but it will get the job done if need be.
  • JSCharts -a JavaScript based chart generator that requires little or no coding. Just include our scripts, prepare your chart data in XML, JSON or JavaScript Array and your chart is ready. Free version watermarked. Paid versions range from $79 – $169 commercial license and $39 – $89 non-profit license.
  • dygraphs – an open source JavaScript library that produces produces interactive, zoomable charts of time series. It is designed to display dense data sets and enable users to explore and interpret them.
  • InfoVis – provides tools for creating Interactive Data Visualizations for the Web. Freeware.
  • g.raphael – Raphaël is a small JavaScript library that should simplify your work with vector graphics on the web and G.Raphaël is specifically for charts. If you want to create your own specific chart or image crop and rotate widget, for example, you can achieve it simply and easily with this library. Very simplified js commands to create graphs and charts. Freeware.
  • sigma – Web network visualization. Freeware. Lightweight JavaScript library to draw graphs, using the HTML canvas element. It has been especially designed to display interactively static graphs exported from a graph visualization software and display dynamically graphs that are generated on the fly.
  • JSXGraph – JSXGraph is a cross-browser library for interactive geometry, function plotting, charting, and data visualization in a web browser. It is implemented completely in JavaScript, does not rely on any other library, and uses SVG, VML, or canvas. JSXGraph is easy to embed and is less than 100 KByte if embedded in a web page. Freeware.
  • Bluff – Bluff is a JavaScript port of the Gruff graphing library for Ruby. It is designed to support all the features of Gruff with minimal dependencies; the only third-party scripts you need to run it are a copy. Freeware.
  • Visualization Free – an easy-to-use, no software download Web-based charting and visualization application. Not for commercial use.
  • Timeline – Interactive widget for visualizing temporal data. Freeware.
  • PlotKit – a Chart and Graph Plotting Library for Javascript. It has support for HTML Canvas and also SVG via Adobe SVG Viewer and native browser support. PlotKit is a complete rewrite of CanvasGraph. It is better structured and supported. Freeware, requires MochiKit 13 or higher.
  • Flot – JavaScript plotting library for jQuery, with a focus on simple usage, attractive looks and interactive features. Freeware.
  • Sparklines – generates small inline charts directly in the browser using data supplied either inline in the HTML, or via javascript.
  • MilkChart – This library will generate a graph similar to Microsoft Excel. Freeware.
  • Canvas 3D Graph – this is the only 3D graphing software I could find. Freeware.
  • TufteGraph – make graphs using jQuery. Configuration is by dynamic functions, allowing for a really compact API (very few options). Non-core layout is done via CSS rather than code. Freeware.
  • MooChart – is a mooTools plugin to draw bubble graphs on the canvas.
  • Protovis – Protovis composes custom views of data with bars and dots. Protovis defines marks through dynamic properties that encode data, allowing inheritance, scales and layouts to simplify construction. Freeware.
  • Rickshaw – an open source javascript toolkit for creating interactive time series graphs
  • Google Charts – not exactly perfect but sometimes the KIS (keep it simple) rule works best.
  • Protochart – open source library using Prototype and Canvas to create good looking charts. This library is highly motivated by Flot, Flotr and PlotKit libraries.