Pen Settings

HTML

CSS

CSS Base

Vendor Prefixing

Add External Stylesheets/Pens

Any URLs added here will be added as <link>s in order, and before the CSS in the editor. You can use the CSS from another Pen by using its URL and the proper URL extension.

+ add another resource

JavaScript

Babel includes JSX processing.

Add External Scripts/Pens

Any URL's added here will be added as <script>s in order, and run before the JavaScript in the editor. You can use the URL of any other Pen and it will include the JavaScript from that Pen.

+ add another resource

Packages

Add Packages

Search for and use JavaScript packages from npm here. By selecting a package, an import statement will be added to the top of the JavaScript editor for this package.

Behavior

Auto Save

If active, Pens will autosave every 30 seconds after being saved once.

Auto-Updating Preview

If enabled, the preview panel updates automatically as you code. If disabled, use the "Run" button to update.

Format on Save

If enabled, your code will be formatted when you actively save your Pen. Note: your code becomes un-folded during formatting.

Editor Settings

Code Indentation

Want to change your Syntax Highlighting theme, Fonts and more?

Visit your global Editor Settings.

HTML

              
                <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <link rel="stylesheet" href="./main.css">
    <title>Backdraft Polygraph Example Application</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
              
            
!

CSS

              
                @charset "UTF-8";

body {
    font-family: Helvetica Neue, Arial, sans-serif;
}

polygon {
    fill: #42b983;
    opacity: .75;
}

circle {
    fill: transparent;
    stroke: #999;
}

text {
    font-family: Helvetica Neue, Arial, sans-serif;
    font-size: 10px;
    fill: #666;
}

label {
    display: inline-block;
    margin-left: 10px;
    width: 20px;
}

div.value {
    display:inline-flex;
    width:3em;
    justify-content: center;
}

.raw {
    position: absolute;
    top: 0;
    left: 300px;
}


              
            
!

JS

              
                import {Component, Collection, CollectionChild, render, svg, e, toWatchable} from "https://unpkg.com/bd-core@2.3.1/lib.js";

// This is a port of the Vue example of the same name.
// Other than the mathematical calculation of a point (which is a property of mathematics, not the example), it shares
// no common code.

// The application consists of
//   * a polygraph--an svg polygon drawn inside a circle with labeled points: implemented as the PolyGraph component
//   * a collection of controllers for each point: implemented as a Collection of StatController components
//   * a little form to add a stat: part of the Page component
//   * some text that reflects the JSON value of stats: part of the Page component
//
// In addition to playing with the controllers, you can play with the underlying data in the console via the variable
// window.polygraphData. For example, try typing "window.polygraphData.reverse()" into the console to reverse the order
// of the points.
//
// To grok the program look at Polygraph, then AxisLabel, then StatController, and finally Page.

// The raw data to observe
// window.polygraphData allows the data to be mutated in the debug console
let stats = window.polygraphData = toWatchable([
	{label: "A", value: 100},
	{label: "B", value: 100},
	{label: "C", value: 100},
	{label: "D", value: 100},
	{label: "E", value: 100},
	{label: "F", value: 100}
]);

function valueToPoint(value, index, total){
	let x = 0;
	let y = -value * 0.8;
	let angle = Math.PI * 2 / total * index;
	let cos = Math.cos(angle);
	let sin = Math.sin(angle);
	return {x: Math.round(x * cos - y * sin + 100), y: Math.round(x * sin + y * cos + 100)};
}

class AxisLabel extends CollectionChild.withWatchables("item:label, value", "point") {
	// AxisLabel is used by Polygraph to output the labels in the polygraph. Polygraph is given an watchable array of
	// "stats" of {label: string, value: number}; see Polygraph. Each item in the array causes one AxisLabel instance to
	// be created that's associated with one stat item.
	//
	// AxisLabel uses mathematics (see valueToPoint, above) to create an (x, y) point given a value, and then renders a
	// a svg text node with the x and y attributes reflecting the point and innerHTML reflecting the label.
	//
	// Backdraft reflection is used to automatically update the point any time any of the inputs into valueToPoint mutate,
	// and then update the svg text node's x any y attributes any time the point changes. Similarly, the svg's text node
	// is automatically updated any time the label mutates. The superclass, CollectionChild.withWatchables, causes the
	// watchable properties label, value, and point to be declared on each AxisLabel instance. Further, label and value
	// are actually virtual properties, simply reflecting the label and value properties of the particular stat to which
	// a particular AxisLabel instance is associated.
  
  constructor(kwargs){
    super(kwargs);
    
    // initialize the point property, given the value property
    // withWatchables("item:label, value") ensures that this.label and this.value reflect the stat item's value and label properties
    this.onMutateValue(this.value);
  }

	bdElements(){
		// reflect the value of this.label into a svg text node
		return svg("text", {
			bdReflect: {
				// update the svg text node's x attribute any time this.point.x changes
				x: ["point", p => p.x],

				// update the svg text node's y attribute any time this.point.y changes
				y: ["point", p => p.y],

				// update the svg text node's innerHTML any time this.label changes
				innerHTML: "label"
			}
		});
	}

	onMutateValue(value){
		// anytime item.value mutates, we must update the point
		this.point = valueToPoint((value || 0) + 10, this.collectionIndex, this.collectionLength);
	}

	onMutateCollectionLength(){
		// anytime the length of the collection changes, we must update the point
		this.onMutateValue(this.value);
	}

	onMutateCollectionIndex(){
		// anytime the array index of the item associated with this instance changes, we must update the point.
		// This can happen when points are re-ordered...because Backdraft *does not* destroy/create DOM nodes consequent
		// to reordering. You can test this by opening the console after loading the app, and executing 
		// "window.polygraphData.reverse()", which will reverse the data in the array.  Backdraft does all of this
    // *without* a virtual dom, *without* adding keys to the array items...indeed...without "re-rendering"
		// but rather updating the already rendered document fragment!
		this.onMutateValue(this.value);
	}
}

class Polygraph extends Component {
	// Polygraph is an svg drawing of a polygon that consists of the (x, y) points computed by pointsToPolyPoints given
	// an array of "stats" of {label: string, value: number}. The array is provided as a keyword argument at construction
	// in the property "stats" and is a watchable array. Polygraph takes advantage of this feature by updating the
	// svg polygon points attribute any time any value in stats changes. In particular,
	//
	// bdReflect_points: [this.kwargs.stats, pointsToPolyPoints]
	//
	// instructs Backdraft to reflect the value of pointsToPolyPoints(this.kwargs.stats) into the points attribute
	// of the polygon node any time this.kwargs.stats mutates.
	//
	// Polygraph also draws a circle and includes a Collection of AxisLabels, one AxisLabel for each item in stats. The
	// Backdraft class Collection is used to manage a set of homogeneous components that reflect a collection (actualized
	// as an array) of homogeneous data--a very common pattern. Collection ensures that a child item is created/mutated/deleted
	// to reflect the underlying data...all using algorithms as efficient as hand-tuned code.

	bdElements(){
		function pointsToPolyPoints(stats){
			let length = stats.length;
			return stats.map((s, i) => valueToPoint(s.value, i, length)).map(p => p.x + "," + p.y).join(" ");
		}

		return svg("svg", {width: 200, height: 200},
			svg("g",
				svg("polygon", {bdReflect_points: [this.kwargs.stats, pointsToPolyPoints]}),
				svg("circle", {cx: 100, cy: 100, r: 80}),
				e(Collection, {elements: svg("g"), childType: AxisLabel, collection: this.kwargs.stats})
			)
		);
	}
}

class StatController extends CollectionChild.withWatchables("item:label, value", "point") {
	// This is the component that has the slider label, value, and delete button; 
  // it is implemented using the same ideas as AxisLabel.
  
	bdElements(){
		return e("div",
			e("label", {
				// when the label property mutates in the stat item associated with this instance,
				// reflect the new value into this node.
				bdReflect: "label"
			}),
			e("input", {
				type: "range", min: 0, max: 100, value: this.collectionItem.value,

				// when the input node signals an event of type "input",
				// set the value property of the stat item associated with this instance
				bdOn_input: e => (this.collectionItem.value = Number(e.target.value)),

				// when the value property mutates in the stat item associated with this instance,
				// reflect the new value into the value attribute of this node.
				bdReflect_value: "value"
			}),
			e("div", {
				className: "value",

				// when the value property mutates in the stat item associated with this instance,
				// reflect the new value into this node.
				bdReflect: "value"
			}),
			e("button", {
				className: "remove",

				bdOn_click: () => {
					// when the button node signals an event of type "click", try to delete this item from the underlying data
					// notice that, given our design that *reflects* the data, we simply manipulate the underlying data
					// and the user interface automatically synchronizes...all without virtual dom!
					if(this.parent.collection.length > 3){
						this.parent.collection.splice(this.collectionIndex, 1);
					}else{
						alert("Can't delete more!");
					}
				}
			}, "X")
		);
	}
}

class Page extends Component {
	// Here's the whole application...it consists of a Polygraph instance, a Collection of StatController instances, a little
	// form to add a stat, and some text that reflects the JSON value of stats.
	//
	// When a new Page instance is instantiated, it is passed the stats array in the keyword constructor argument "stats".

	bdElements(){
		return e("div",
			e(Polygraph, {bdAttach: "graph", stats: this.kwargs.stats}),
			e(Collection, {childType: StatController, collection: this.kwargs.stats}),
			e("form",
				e("input", {bdAttach: "input"}),
				e("button", {bdAdvise: {"click": this.add.bind(this)}}, "Add a Stat")
			),
			e("pre", {
				className: "raw",
				bdReflect: [this.kwargs.stats, () => JSON.stringify(stats, null, "\t")]
			})
		);
	}

	add(e){
		e.preventDefault();
		let label = this.input.value;
		if(!label) return;
		this.kwargs.stats.push({label: label, value: 100});
		this.input.value = "";
	}
}


render(Page, {stats: stats}, "root");


              
            
!
999px

Console