diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2017 sen.cenan@gmail.com
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be included
+in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
+IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
+CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
+TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/Main.hs b/Main.hs
new file mode 100644
--- /dev/null
+++ b/Main.hs
@@ -0,0 +1,50 @@
+module Main where
+
+import Data.Semigroup ((<>))
+
+import qualified Options.Applicative as Opt
+
+import Network.Socketed.Internal (stdinPassthrough)
+import Network.Socketed.Application.Plocketed.Template (plotHtml)
+import Network.Socketed.Application.Plocketed.Internal (PlocketedOptions(..))
+
+import System.IO (hFlush, stdout)
+
+params :: Opt.Parser PlocketedOptions
+params = PlocketedOptions
+   <$> Opt.option Opt.auto
+      ( Opt.long "width"
+      <> Opt.short 'x'
+      <> Opt.help "width of the chart"
+      <> Opt.showDefault
+      <> Opt.value 1100
+      <> Opt.metavar "INT" )
+   <*> Opt.option Opt.auto
+      ( Opt.long "height"
+      <> Opt.short 'y'
+      <> Opt.help "height of the chart"
+      <> Opt.showDefault
+      <> Opt.value 600
+      <> Opt.metavar "INT")
+   <*> Opt.option Opt.auto
+      ( Opt.long "maxData"
+      <> Opt.short 'm'
+      <> Opt.help "max data point kept"
+      <> Opt.showDefault
+      <> Opt.value 20
+      <> Opt.metavar "INT")
+
+main :: IO ()
+main = Opt.execParser opts >>= \x -> do
+      let s = plotHtml x
+      putStrLn s
+      putStrLn "\n" -- kick off socketed
+      hFlush stdout
+      stdinPassthrough
+   where
+      opts = Opt.info (Opt.helper <*> params)
+         (
+            Opt.fullDesc
+            <> Opt.progDesc "plocketed"
+            <> Opt.header "plocketed"
+         )
diff --git a/Network/Socketed/Application/Plocketed/Internal.hs b/Network/Socketed/Application/Plocketed/Internal.hs
new file mode 100644
--- /dev/null
+++ b/Network/Socketed/Application/Plocketed/Internal.hs
@@ -0,0 +1,7 @@
+module Network.Socketed.Application.Plocketed.Internal where
+
+data PlocketedOptions = PlocketedOptions {
+      width :: Int,
+      height :: Int,
+      maxData :: Int
+   }
diff --git a/Network/Socketed/Application/Plocketed/Template.hs b/Network/Socketed/Application/Plocketed/Template.hs
new file mode 100644
--- /dev/null
+++ b/Network/Socketed/Application/Plocketed/Template.hs
@@ -0,0 +1,171 @@
+{-# LANGUAGE QuasiQuotes #-}
+
+module Network.Socketed.Application.Plocketed.Template (plotHtml) where
+
+import Data.List (intercalate)
+
+import Network.Socketed.Internal (stringQuote)
+import Network.Socketed.Application.Plocketed.Internal (PlocketedOptions(..))
+
+-- width
+-- height
+-- data points to keep
+
+plotHtml :: PlocketedOptions -> String
+plotHtml (PlocketedOptions w h m) = intercalate "" $ lines $ [stringQuote|
+
+(function(width, height, maxDataAmount) {
+
+var script = document.createElement('script');
+script.src = '//www.chartjs.org/assets/Chart.min.js';
+
+script.onload = function() {
+   var canvas = document.createElement('canvas');
+   canvas.setAttribute('width', width);
+   canvas.setAttribute('height', height);
+   document.body.appendChild(canvas);
+
+   var legend = document.createElement('div');
+   document.body.appendChild(legend);
+
+   var
+      ctx = canvas.getContext('2d'),
+      seq = 0,
+      dataMap = {}, /* data key to data set */
+      chart;
+
+   var colors = [
+      '#00ffff', '#f0ffff', '#f5f5dc', '#000000', '#0000ff', '#a52a2a',
+      '#00ffff', '#00008b', '#008b8b', '#a9a9a9', '#006400', '#bdb76b',
+      '#8b008b', '#556b2f', '#ff8c00', '#9932cc', '#8b0000', '#e9967a',
+      '#9400d3', '#ff00ff', '#ffd700', '#008000', '#4b0082', '#f0e68c',
+      '#add8e6', '#e0ffff', '#90ee90', '#d3d3d3', '#ffb6c1', '#ffffe0',
+      '#00ff00', '#ff00ff', '#800000', '#000080', '#808000', '#ffa500',
+      '#ffc0cb', '#800080', '#800080', '#ff0000', '#c0c0c0'
+   ];
+
+   var randomColorFrag = function() {
+      var c = colors[
+         Math.floor(Math.random() * colors.length * 10) % colors.length
+      ].substring(1);
+
+      return 'rgba('
+         + [c.substring(0,2), c.substring(2,4), c.substring(4)]
+            .map(v => parseInt(v, 16))
+            .join(',');
+   };
+
+   var blend = function(frag, a) {
+      return frag + ',' + a + ')';
+   };
+
+   var mkDataSet = function(label) {
+      var c = randomColorFrag();
+
+      return {
+        fillColor: blend(c, .2),
+        strokeColor: blend(c, .7),
+        pointColor: blend(c, .1),
+        pointStrokeColor: "#fff",
+        points: [],
+        label: label
+      };
+   };
+
+   var mkChart = function() {
+      if (chart) {
+         chart.clear();
+         chart.destroy();
+      }
+
+      seq = 0;
+      var ds = Object.keys(dataMap)
+         .map(k => dataMap[k])
+         .sort((a, b) => a.idx - b.idx)
+         .map(set => {
+
+            /* trim to the last element */
+            set.data = [set.data[set.data.length - 1]];
+
+            return Object.assign(
+               mkDataSet(set.key),
+               { data: set.data }
+            );
+         });
+
+      console.log(JSON.stringify(ds));
+
+      chart
+         = window.chart
+         = new Chart(ctx).Line(
+            {
+               labels: [seq],
+               datasets: ds
+            },
+            {
+               animationSteps: 15,
+               legendTemplate: '<table><tr>'
+                   +'<% for (var i=0; i<datasets.length; i++) { %>'
+                   +'<td><div class=\"boxx\" '
+                     + 'style=\"background-color:<%=datasets[i].fillColor %>\">'
+                   +'<% if (datasets[i].label) '
+                     + '{ %> <%= datasets[i].label %> <% } %></div></td>'
+                   +'<% } %>'
+                   +'</tr></table>'
+            }
+         );
+
+      legend.innerHTML = chart.generateLegend();
+   };
+
+   window.dataMap = dataMap;
+
+   window.handleMessage = function(e) {
+      var
+         vals = e.data.split(' ').map(x => x.trim()).map(x => parseInt(x, 10)),
+         key = vals[0],
+         val = vals[1];
+
+      if (!(key in dataMap)) {
+         dataMap[key] = {
+            key: key,
+            idx: Object.keys(dataMap).length,
+            data: [val]
+         };
+
+         mkChart();
+      } else {
+         var newPoints = Object.keys(dataMap)
+            .map(k => dataMap[k])
+            .sort((a, b) => a.idx - b.idx)
+            .map(set => {
+               if (set.key === key) {
+                  set.data.push(val);
+               } else {
+                  set.data.push(set.data[set.data.length - 1]);
+               }
+
+               return set.data[set.data.length - 1];
+            });
+
+         chart.addData(newPoints, ++seq);
+      }
+
+      if (seq > maxDataAmount) {
+         chart.removeData();
+
+         newPoints = Object.keys(dataMap).map(k => {
+            dataMap[k].data.shift();
+         });
+      }
+   };
+
+   window.handleMessageError = function(e, err) {
+      console.log(e, err);
+   };
+};
+
+document.body.appendChild(script);
+
+})(
+|] ++ (intercalate "," $ map show [w, h, m]) ++ ");"
diff --git a/plocketed.cabal b/plocketed.cabal
new file mode 100644
--- /dev/null
+++ b/plocketed.cabal
@@ -0,0 +1,26 @@
+name: plocketed
+version: 0.1.0.0
+
+synopsis: plot data from stdin through socketed
+description: plot data from stdin through socketed
+
+license: MIT
+license-file: LICENSE
+author: sen.cenan@gmail.com
+maintainer: sen.cenan@gmail.com
+
+category: Web
+build-type: Simple
+cabal-version: >=1.10
+
+executable plocketed
+  main-is: Main.hs
+  default-language: Haskell2010
+  build-depends:
+    base >=4.9 && <4.10,
+    socketed >= 0.1.0.0,
+    optparse-applicative >= 0.13.2.0
+  ghc-options: -threaded
+  other-modules:
+    Network.Socketed.Application.Plocketed.Internal,
+    Network.Socketed.Application.Plocketed.Template
