Skip to content

Instantly share code, notes, and snippets.

@bollwyvl
Forked from tonyfast/d3.ml.js
Last active September 1, 2016 17:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save bollwyvl/172e330701978a831074 to your computer and use it in GitHub Desktop.
Save bollwyvl/172e330701978a831074 to your computer and use it in GitHub Desktop.

d3.ml

A markup language on top of d3 to combine data and DOM elements.

Currently, all the selection and data API methods from d3 are available.

Parent Nodes

classed .d3-ml

;( function(){
// an extension for d3 that iterates over Javascript objects
// to build a DOM derived from data.
d3.ml = {
// YAML templates that execute d3ml
templates: {},
requests: {},
scripts: {},
build:
function(s, template, state){
// for a selection build a template's state
// with available requests, templates, and scripts
if ( !s.data()[0]){
// make sure the parent selection has
// data for the worker to use.
s = s.datum( {} );
}
if ( d3.ml.templates[template][state] ){
// add a class to the parent selection so it knows d3ml was used
s.classed('d3-ml',true);
// build the template only if it exists
return d3.ml.worker(
s,
d3.ml.templates[template][state]
)
} else {
console.log( 'There is no template, ' + template + ',with the state,' + state +'.' );
}
},
worker:
function ( s, template ){
// Execute a d3ml template on the
// selection
if ( s.data()[0] ){
// if a selection has data then
// bring it into the scope
var data = s.data()[0];
}
template.forEach( function(template){
// for each of selections
// traverse the templates with
// d3ml
s = d3.ml.task( s, d3.entries(template)[0], data)
})
return s;
},
helper: {
extend :
function ( d, _d, i ){
// a hack for $.extend with native d3
if ( !Array.isArray(d) ){
d3.merge( [
d3.entries( d ),
d3.entries( _d )
])
.forEach( function(_d){
i[ _d.key] = _d.value;
})
return i;
} else {
// Don't append objects to Arrays
return d;
}
},
extend:
function ( d, _d, i ){
// a hack for $.extend with native d3
if ( !Array.isArray(d) ){
d3.merge( [
d3.entries( d ),
d3.entries( _d )
])
.forEach( function(_d){
i[ _d.key] = _d.value;
})
return i;
} else {
// Don't append objects to Arrays
return d;
}
},
intersect :
function (str, set ){
// return true if a string is in a set of strings
return(
set.filter( function(d){
return (d == str);
}).length > 0
)
},
reduce:
function ( k, d, _d ){
// get the value of a key for
// either the current scope ':' or local scope '@'
if (k[0] == '@'){
// return data from the local scope
d = _d;
}
if ( d3.ml.helper.intersect( k , [':','@'] ) ){
return d
} else if ( d3.ml.helper.intersect( k[0] , [':','@'] ) ){
return k.slice(1).split('.')
.reduce( function( p, n){
if (p[n]){
// recurse object through intersect
return p[n]
} else {
// default if key doesn't exist
return {}
}
}, d );
} else {
return k;
}
}
},
task: function ( s, t, data ){
// for a selection, s, apply a action defined t
// using data if it is needed
if ( t.key == 'call' ){
// update selection and the data scope is updated in the worker
s = s[t.key]( function(s){
d3.ml.worker(s, t.value);
})
} else if ( t.key == 'template' ){
// send a nested template to the worker to execute
s = d3.ml.worker(
s,
d3.ml.helper.reduce(
t.value,
d3.ml.templates
)
)
} else if ( t.key == 'each' ){
// iterate over a multi-selection array, d3 selection
s = s[t.key]( function(){
d3.ml.worker(d3.select(this), t.value);
})
} else if ( t.key == 'class' ){
// Append classes to the selection.
// d.value is a object that is iterated over
d3.entries( t.value )
.forEach( function(_d){
if (_d.value == null ){
s.classed( _d.key, true )
} else {
s.classed( _d.key, _d.value )
}
})
} else if (
d3.ml.helper.intersect( t.key, ['attr','style','property'] )
){
// change the style of attributes
// values are objects like class
d3.entries( t.value )
.forEach( function(_d){
s[t.key]( _d.key, function(__d){
return (
d3.ml.helper.reduce( _d.value, data, __d )
)
})
})
} else if (
d3.ml.helper.intersect( t.key, ['enter','exit','remove'] )
){
// modified data in selections
s = s[t.key]()
} else if (
d3.ml.helper.intersect( t.key, ['data'] )
){
// update data in for a selection
s = s[t.key]( function(_d){
return d3.ml.helper.reduce( t.value, data, _d )
})
} else if (
d3.ml.helper.intersect( t.key, ['xml','json','yaml','yml','aml','plain','csv','tsv'] )
){
// append data from the archie base
// update selection
var parse = function(d){ return d;}
if ( d3.ml.helper.intersect( t.key, ['yaml','yml' ] ) ){
t.key = 'text'
parse = function(d){
return jsyaml.load(d);
}
}
if ( d3.ml.helper.intersect( t.key, ['aml' ] ) ){
t.key = 'text'
parse = function(d){
return archieml.load(d);
}
}
if ( d3.ml.helper.intersect( t.key, ['plain' ] ) ){
t.key = 'text'
}
d3[t.key]( t.value, function(d){
if ( t.key == 'aml' ){
if( window['archieml']){
d = archieml.load( d )
}
}
s = d3.ml.task( s, {
key: 'datum',
value: parse(d)
}, data)
})
} else if ( d3.ml.helper.intersect( t.key, ['datum'] ) ){
// append data-on-the-fly
s = s[t.key]( function(_d){
// previously attached data
if (_d ){
// merge objects
if (typeof t.value == 'string'){
// access predefined variables
// allows arbitrary data
t.value = d3.ml.helper.reduce( t.value, data, _d)
}
return (
d3.ml.helper.extend( _d, t.value, {} )
)
} else {
// attach data if it hasnt been assigned
return (_d);
}
})
} else if ( d3.ml.helper.intersect( t.key, ['selectAll','select'] ) ){
// selection changes
s = s[t.key]( t.value );
} else if ( d3.ml.helper.intersect( t.key, ['append','insert'] ) ){
// selection changes
if (t.value[0] == '$'){
// I wish i knew regular expressions
// this can probably be done with the function update by converting to a template
// $tag.class1-name.class2-name#id
var path = t.value.slice(1).split('.');
if ( (path[0].length > 0) && ( path[0][0] != '#' ) ){
// dont forget to update teh selection
s = s.append( path[0] )
}
// add classes
path.slice(1).filter( function(path){
return (path[0] != '#') && ( path[0].length > 0 )
}).forEach( function(path){
s.classed( path.split('#')[0], true);
})
path.filter( function(path){
return (path.split('#')[1])
}).forEach( function(path){
// this will only happen once
s.attr( 'id', path.split('#')[1] );
})
} else {
// normal append
s = s[t.key]( t.value );
}
} else if ( d3.ml.helper.intersect( t.key, ['text','html'] ) ){
// append data from the d3 base
s[t.key]( function(_d){
return d3.ml.helper.reduce( t.value, data, _d)
})
} else {
// don't use any other commands yet
}
return s;
}
}
})();
<!DOCTYPE html>
<html lang="en">
<head>
<style>
@import "https://cdnjs.cloudflare.com/ajax/libs/materialize/0.95.3/css/materialize.min.css";
@import "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.0.0/codemirror.min.css";
@import "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.0.0/theme/blackboard.css";
@import "https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.0.0/addon/fold/foldgutter.css";
body {
font-size: 12px;
}
#toolbar{
position: fixed;
right: 0;
top: 0;
}
#toggle {
margin-top: 1.25em;
}
#template {
position: fixed;
right: 0;
width: 400px;
top: 68px;
height: 100%;
opacity: 0.7;
}
.CodeMirror {
height: auto;
}
</style>
</head>
<body>
<div id="preview" class="row">
<div class="container">
<h5>This tool creates webpages by the key stroke.</h5>
<p class="flow-text">Mark up the YAML editor with d3-like commands to build a DOM with inline data.</p>
</div>
</div>
<div id="template" class="mode"></div>
<div id="toolbar" class="col">
<div class="row">
<a id="toggle" class="waves-effect waves-light btn col s2">
<i class="mdi-image-edit"></i>
</a>
<div class="input-field col s10 download">
<i class="mdi-file-file-download prefix"></i>
<input id="template-url" type="text" class="validate" value="./templates.yml" >
<label for="template-url">Template URL</label>
</div>
</div>
</div>
<script src="//cdn.jsdelivr.net/g/d3js,jquery">
</script>
<script src="//cdnjs.cloudflare.com/ajax/libs/codemirror/5.0.0/codemirror.js">
</script>
<script src="//cdnjs.cloudflare.com/ajax/libs/codemirror/5.0.0/mode/yaml/yaml.js">
</script>
<script src="//cdnjs.cloudflare.com/ajax/libs/codemirror/5.0.0/addon/fold/foldcode.js">
</script>
<script src="//cdnjs.cloudflare.com/ajax/libs/codemirror/5.0.0/addon/fold/foldgutter.js">
</script>
<script src="//cdnjs.cloudflare.com/ajax/libs/codemirror/5.0.0/addon/fold/indent-fold.js">
</script>
<script src="//cdnjs.cloudflare.com/ajax/libs/codemirror/5.0.0/addon/fold/comment-fold.js">
</script>
<script src="//cdnjs.cloudflare.com/ajax/libs/materialize/0.95.3/js/materialize.min.js">
</script>
<script src="//cdnjs.cloudflare.com/ajax/libs/js-yaml/3.2.7/js-yaml.min.js">
</script>
<script src="d3.ml.js">
</script>
<script>
;( function(){
var toolbarHeight = 68,
showEditor = 1;
var template = d3.select('#template'),
preview = d3.select('#preview'),
templateUrl = d3.select("#template-url"),
win = d3.select(window);
var editor = CodeMirror(template.node(), {
theme: "blackboard",
mode: "yaml",
lineNumbers: true,
lineWrapping: true,
extraKeys: {"Ctrl-Q": function(cm){ cm.foldCode(cm.getCursor()); }},
foldGutter: {
rangeFinder: new CodeMirror.fold.combine(CodeMirror.fold.indent, CodeMirror.fold.comment)
},
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"]
});
d3.selectAll('#toggle')
.on('click', function(){
template.style('display',
(showEditor = !showEditor) ? "block" : "none"
);
});
editor.on('change', update);
win.on("resize", resize);
templateUrl.on("change", updateHash);
win.on("hashchange", load)
win.on("resize")();
if(hash()){
templateUrl.property("value", hash());
}
templateUrl.on("change")();
function hash(value){
if(value){
window.location.hash = value;
}
return window.location.hash.slice(1);
}
function updateHash(){
hash(templateUrl.property("value"));
win.on("hashchange")()
}
function load(){
d3.text(hash(), 'text/yaml', function(d){
editor.setValue(d);
});
}
function update(){
d3.ml.templates = jsyaml.load( editor.getValue() );
preview.call( function(s){
s.html('')
d3.entries( d3.ml.templates['display'] )
.forEach( function(d){
d3.ml.build( s, d.key, d.value )
});
});
}
function resize(){
editor.setSize(
null,
(win.node().innerHeight - toolbarHeight) + "px"
)
}
})();
</script>
</body>
</html>
display:
card: mount
card-title:
mount:
- call:
- append: $span.card-title
- text: ':title'
- call:
- append: p
- text: ':content'
card-content:
mount:
- append: $div.card-content.white-text
- call:
- template: ':card-title.mount'
card-action:
mount:
- append: $div.card-action
- selectAll: a
- data:
- 1
- 2
- call:
- enter:
- append: a
- each:
- text: ':'
- attr:
href: ':'
card:
mount:
- append: $div.row
- append: $div.col.s12.m6
- append: $div.card.blue.darken-1
- datum:
title: Title
content: This is the content
- call:
- call:
- template: ':card-content.mount'
- call:
- template: ':card-action.mount'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment