<commit_before>let Firebase = require('firebase');
let html = require('./html-escaping');
let welcome = require('./welcome')();
let base = new Firebase('https://scorching-inferno-1467.firebaseio.com/');

let auth = base.getAuth();

const setTitle = () => {
  document.getElementById('company-name').innerHTML = JSON.parse(localStorage.punchtime).company.name;
};

// if the localstorage hasn't been set, go to settings
if (!localStorage.punchtime) {
  location.href = 'settings.html';
}

if (auth) {
  console.log('logged in with: ' + auth.uid);
  setTitle();
} else {
  console.warn('not logged in');
  location.href = '/login/';
}

let pulses = []; // the graph is made from this object

const addEmployee = (employee) => {
  let employeePulses = [];
  for (let pulse in employee.pulses) {
    if (employee.pulses.hasOwnProperty(pulse)) {
      base.child('pulses').child(pulse).once('value', snap => {
        try {
          let name = employee.name,
            checkin = new Date(parseInt(snap.val().checkin)),
            note = snap.val().note,
            address = snap.val().addressStreet,
            checkout;
          if (snap.val().checkout && snap.val().checkout !== 0) {
            checkout = new Date(parseInt(snap.val().checkout));
          } else {
            checkout = new Date();
          }
          pulses.push([name, address, checkin, checkout]);
          employeePulses.push({
            id: snap.key(),
            latitude: snap.val().latitude,
            longitude: snap.val().longitude,
            checkin: new Date(parseInt(snap.val().checkin)),
            checkout: new Date(parseInt(snap.val().checkout)),
            address: snap.val().addressStreet,
            note: snap.val().note,
            confirmed: snap.val().confirmed
          });
          drawChart();
        } catch (e) {
          console.log(`/pulses/${snap.key()} in /users/${employee.name} has a problem.`);
        }
      });
    }
  }

  let image = employee.image || '/src/img/icons/empty.svg',
    name = employee.name,
    status = 'good';

  let empl = document.createElement('div');
  empl.classList.add('employee');
  // empl.dataset.id = id;
  empl.innerHTML = html `

${name}

${status}`;
  document.querySelector('.employee-container').appendChild(empl);

  empl.addEventListener('click', () => {
    let overview = document.createElement('div');
    overview.classList.add('overview');
    // todo: get the right pulses out of the user
    let overviewContent = document.createElement('div');
    overviewContent.classList.add('overview--content');
    overviewContent.innerHTML = html `<h2 class="overview--title">${employee.name}</h2>`;
    let timeline = document.createElement('div');
    timeline.classList.add('timeline');
    timeline.innerHTML = html `

Thursay, 28 April

Werfstraat 131

there is some note

    <time datetime="2016-04-28T15:15:24+00:00">15:15</time>→
    <time datetime="2016-04-28T15:30:24+00:00">15:30</time>

30 min

Oudeheerweg 13

this is also a note

    <time datetime="2016-04-28T16:00:24+00:00">16:00</time>→
    <time datetime="2016-04-28T16:30:24+00:00">16:30</time>

`;

    // todo: don't hardcode this anymore
    let previous = {
      checkout: new Date(1462022458815)
    }
    let current = {
      checkin: new Date(1462022569815),
      checkout: new Date(1462022697858),
      id: '-KGbKsOy2jwSbMK-QdFP',
      confirmed: 'true',
      note: 'bla bla bla bla',
      address: 'Gebroeders Desmetstraat 1'
    };
    let diff = new Date(current.checkin - previous.checkout);

    timeline.innerHTML += html `

${diff.getHours() - 1}h ${diff.getMinutes() + Math.round(diff.getSeconds() / 60)}m

${current.address}

${current.note}

    <time datetime="${current.checkin.toISOString()}">${current.checkin.getHours()}:${current.checkin.getMinutes()}</time>→
    <time datetime="${current.checkout.toISOString()}">${current.checkout.getHours()}:${current.checkout.getMinutes()}</time>

`;
    overviewContent.appendChild(timeline);
    overview.appendChild(overviewContent);

    document.body.insertBefore(overview, document.querySelector('.employees'));
    overview.addEventListener('click', (e) => {
      if (e.target.classList.contains('overview')) {
        e.target.parentNode.removeChild(e.target);
        console.log('removed');
      } else if (e.target.parentNode.classList.contains('timeline--item__still')) {
        toggleStatus(e.target.parentNode);
      } else if (e.target.classList.contains('timeline--item__still')) {
        toggleStatus(e.target);
      }
    });
  });

  let flexfix = document.createElement('div');
  flexfix.classList.add('👻');
  document.querySelector('.employee-container').appendChild(flexfix);
};

let toggleStatus = (element) => {
  if (element.classList.contains('timeline--item__unconfirmed')) {
    element.classList.remove('timeline--item__unconfirmed');
    element.classList.add('timeline--item__confirmed');
    base.child('pulses')
      .child(element.dataset.pulse)
      .child('confirmed')
      .set('true');
  } else if (element.classList.contains('timeline--item__confirmed')) {
    element.classList.remove('timeline--item__confirmed');
    element.classList.add('timeline--item__unconfirmed');
    base.child('pulses')
      .child(element.dataset.pulse)
      .child('confirmed')
      .set('false');
  }
};

let getEmployees = (id, callback) => {
  base.child('companies').child(id).child('employees').on('child_added', (snapshot) => {
    base.child('users').child(snapshot.key()).once('value', (snap) => {
      callback(snap.val());
    });
  });
};

getEmployees(JSON.parse(localStorage.punchtime).company.id, addEmployee);

const drawChart = () => {
  let chart = new google.visualization.Timeline(document.getElementById('timeline'));
  let dataTable = new google.visualization.DataTable();

  dataTable.addColumn({
    type: 'string',
    id: 'employee'
  });
  dataTable.addColumn({
    type: 'string',
    id: 'note'
  });
  dataTable.addColumn({
    type: 'date',
    id: 'Start'
  });
  dataTable.addColumn({
    type: 'date',
    id: 'End'
  });
  dataTable.addRows(pulses);

  chart.draw(dataTable, {
    timeline: {
      singleColor: '#3B4358'
    }
  });
};
google.charts.load('current', {
  'packages': ['timeline']
});
google.charts.setOnLoadCallback(drawChart);

/*
 * listener on resize to reload graph
 * Will refresh the graph if there is 250 ms after a resize event
 */
var resizeTimer;
window.addEventListener('resize', () => {
  clearTimeout(resizeTimer);
  resizeTimer = setTimeout(() => {
    drawChart();
  }, 250);
});
<commit_msg>make more vertical<commit_after>let Firebase = require('firebase');
let html = require('./html-escaping');
let welcome = require('./welcome')();
let base = new Firebase('https://scorching-inferno-1467.firebaseio.com/');

let auth = base.getAuth();

const setTitle = () => {
  document.getElementById('company-name').innerHTML = JSON.parse(localStorage.punchtime).company.name;
};

// if the localstorage hasn't been set, go to settings
if (!localStorage.punchtime) {
  location.href = 'settings.html';
}

if (auth) {
  console.log('logged in with: ' + auth.uid);
  setTitle();
} else {
  console.warn('not logged in');
  location.href = '/login/';
}

let pulses = []; // the graph is made from this object

const addEmployee = (employee) => {
  let employeePulses = [];
  for (let pulse in employee.pulses) {
    if (employee.pulses.hasOwnProperty(pulse)) {
      base.child('pulses').child(pulse).once('value', (snap) => {
        try {
          let name = employee.name,
            checkin = new Date(parseInt(snap.val().checkin)),
            note = snap.val().note,
            address = snap.val().addressStreet,
            checkout;
          if (snap.val().checkout && snap.val().checkout !== 0) {
            checkout = new Date(parseInt(snap.val().checkout));
          } else {
            checkout = new Date();
          }
          pulses.push([name, address, checkin, checkout]);
          employeePulses.push({
            id: snap.key(),
            latitude: snap.val().latitude,
            longitude: snap.val().longitude,
            checkin: new Date(parseInt(snap.val().checkin)),
            checkout: new Date(parseInt(snap.val().checkout)),
            address: snap.val().addressStreet,
            note: snap.val().note,
            confirmed: snap.val().confirmed
          });
          drawChart();
        } catch (e) {
          console.log(`/pulses/${snap.key()} in /users/${employee.name} has a problem.`);
        }
      });
    }
  }

  let image = employee.image || '/src/img/icons/empty.svg',
    name = employee.name,
    status = 'good';

  let empl = document.createElement('div');
  empl.classList.add('employee');
  // empl.dataset.id = id;
  empl.innerHTML = html `

${name}

${status}`;
  document.querySelector('.employee-container').appendChild(empl);

  empl.addEventListener('click', () => {
    addOverview(employeePulses,employee)
  });

  let flexfix = document.createElement('div');
  flexfix.classList.add('👻');
  document.querySelector('.employee-container').appendChild(flexfix);
};

let addOverview = (pulses,employee) => {
  let overview = document.createElement('div');
  overview.classList.add('overview');
  // todo: get the right pulses out of the user
  let overviewContent = document.createElement('div');
  overviewContent.classList.add('overview--content');
  overviewContent.innerHTML = html `<h2 class="overview--title">${employee.name}</h2>`;
  let timeline = document.createElement('div');
  timeline.classList.add('timeline');
  timeline.innerHTML = html `

Thursay, 28 April

Werfstraat 131

there is some note

    <time datetime="2016-04-28T15:15:24+00:00">15:15</time>→
    <time datetime="2016-04-28T15:30:24+00:00">15:30</time>

30 min

Oudeheerweg 13

this is also a note

    <time datetime="2016-04-28T16:00:24+00:00">16:00</time>→
    <time datetime="2016-04-28T16:30:24+00:00">16:30</time>

`;

  // todo: don't hardcode this anymore
  let previous = {
    checkout: new Date(1462022458815)
  }
  let current = {
    checkin: new Date(1462022569815),
    checkout: new Date(1462022697858),
    id: '-KGbKsOy2jwSbMK-QdFP',
    confirmed: 'true',
    note: 'bla bla bla bla',
    address: 'Gebroeders Desmetstraat 1'
  };
  let diff = new Date(current.checkin - previous.checkout);

  timeline.innerHTML += html `

${diff.getHours() - 1}h ${diff.getMinutes() + Math.round(diff.getSeconds() / 60)}m

${current.address}

${current.note}

    <time datetime="${current.checkin.toISOString()}">${current.checkin.getHours()}:${current.checkin.getMinutes()}</time>→
    <time datetime="${current.checkout.toISOString()}">${current.checkout.getHours()}:${current.checkout.getMinutes()}</time>

`;
  overviewContent.appendChild(timeline);
  overview.appendChild(overviewContent);

  document.body.insertBefore(overview, document.querySelector('.employees'));
  overview.addEventListener('click', (e) => {
    if (e.target.classList.contains('overview')) {
      e.target.parentNode.removeChild(e.target);
    } else if (e.target.parentNode.classList.contains('timeline--item__still')) {
      toggleStatus(e.target.parentNode);
    } else if (e.target.classList.contains('timeline--item__still')) {
      toggleStatus(e.target);
    }
  });
};

let toggleStatus = (element) => {
  if (element.classList.contains('timeline--item__unconfirmed')) {
    element.classList.remove('timeline--item__unconfirmed');
    element.classList.add('timeline--item__confirmed');
    base.child('pulses')
      .child(element.dataset.pulse)
      .child('confirmed')
      .set('true');
  } else if (element.classList.contains('timeline--item__confirmed')) {
    element.classList.remove('timeline--item__confirmed');
    element.classList.add('timeline--item__unconfirmed');
    base.child('pulses')
      .child(element.dataset.pulse)
      .child('confirmed')
      .set('false');
  }
};

let getEmployees = (id, callback) => {
  base.child('companies').child(id).child('employees').on('child_added', (snapshot) => {
    base.child('users').child(snapshot.key()).once('value', (snap) => {
      callback(snap.val());
    });
  });
};

getEmployees(JSON.parse(localStorage.punchtime).company.id, addEmployee);

const drawChart = () => {
  let chart = new google.visualization.Timeline(document.getElementById('timeline'));
  let dataTable = new google.visualization.DataTable();

  dataTable.addColumn({
    type: 'string',
    id: 'employee'
  });
  dataTable.addColumn({
    type: 'string',
    id: 'note'
  });
  dataTable.addColumn({
    type: 'date',
    id: 'Start'
  });
  dataTable.addColumn({
    type: 'date',
    id: 'End'
  });
  dataTable.addRows(pulses);

  chart.draw(dataTable, {
    timeline: {
      singleColor: '#3B4358'
    }
  });
};
google.charts.load('current', {
  'packages': ['timeline']
});
google.charts.setOnLoadCallback(drawChart);

/*
 * listener on resize to reload graph
 * Will refresh the graph if there is 250 ms after a resize event
 */
var resizeTimer;
window.addEventListener('resize', () => {
  clearTimeout(resizeTimer);
  resizeTimer = setTimeout(() => {
    drawChart();
  }, 250);
});
<|endoftext|>


<commit_before>$(function()
{
    var ref = new Firebase("https://scorching-fire-2873.firebaseio.com");

    // load options
    ref.child("options").on("value", function(options)
    {
        options = options.val();

        // set title and meta data
        $("head title").html(options.title);
        $("head meta[name=description]").attr("content", options.description);
        $("head meta[name=keywords]").attr("content", options.keywords);
        $("head meta[name=author]").attr("content", options.author);
    }, function(e)
    {
        console.error(e.code);
    });
});<commit_msg>new items are added to the feed<commit_after>$(function()
{
    var ref = new Firebase("https://scorching-fire-2873.firebaseio.com");

    // load options
    ref.child("options").on("value", function(options)
    {
        options = options.val();

        // set title and meta data
        $("head title").html(options.title);
        $("head meta[name=description]").attr("content", options.description);
        $("head meta[name=keywords]").attr("content", options.keywords);
        $("head meta[name=author]").attr("content", options.author);
    }, function(e)
    {
        console.error(e.code);
    });

    // load feed
    ref.child("feed").on("child_added", function(snapshot, prev)
    {
        id   = snapshot.key();
        data = snapshot.val();

        item = $("

");
        switch(data.type)
        {
            case "image":
                $("", {alt: "image" + id, src: data.src}).appendTo(item);
                break;
            case "text":
                $("

").text(data.text).appendTo(item);
                break;
        }

        $("div#feed").prepend(item);
    });
});<|endoftext|>


<commit_before>import { Colors } from './colors';
import debounce from './debounce';
<<<<<<>>>>>> babf8c52e3dd5590f5477db0cb76551ee30e27e3

const [COLOR_DESKTOP, COLOR_MOBILE, COLOR_DESKTOP_ALT, COLOR_MOBILE_ALT] = Colors.getAll({rgba: true});

function histogram(metric, date, options) {
	options.date = date;
<<<<<<>>>>>> babf8c52e3dd5590f5477db0cb76551ee30e27e3
	const dataUrl = `https://cdn.httparchive.org/reports/${date}/${metric}.json`;
	fetch(dataUrl)
		.then(response => {
			if (!response.ok) {
				console.error('Error loading histogram data', dataUrl, response);
				return Promise.reject(response.statusText);
			}
			return response.text();
		})
		.then(jsonStr => JSON.parse(jsonStr))
		.then(data => {
			drawHistogram(data, `${metric}-chart`, options);
			drawHistogramTable(data, `${metric}-table-desktop`, `${metric}-table-mobile`, options.type);
		}).catch(e => {
			const chart = document.getElementById(`${metric}-chart`);
			chart.innerText = `Error loading data: ${e}. Try a more recent start date.`;
		});
}

class Bin {
	constructor(data) {
		this.client = data.client;
		this.bin = +data.bin;
		this.volume = +data.volume;
		this.pdf = +data.pdf;
		this.cdf = +data.cdf;
	}

	toPoint() {
		return [this.bin, this.volume];
	}

	toCdfPoint() {
		return [this.bin, this.cdf * 100];
	}

	toRow(maxPdf) {
		const tr = document.createElement('tr');
		this.getSchema().forEach(col => {
			const td = document.createElement('td');
			if (col === 'volume') {
				const bar = document.createElement('div');
				bar.style.width = (this.pdf * 100 / maxPdf) + '%';
				bar.classList.add(this.client);
				td.appendChild(bar);
				const text = document.createElement('span');
				text.textContent = this.format(col);
				td.appendChild(text);
			}
			else {
				td.textContent = this.format(col);
			}
			td.classList.add(col);
			tr.appendChild(td);
		})
		return tr;
	}

	add(bin) {
		this.volume += bin.volume;
		this.pdf += bin.pdf;
		this.cdf = Math.max(this.cdf, bin.cdf);
	}

	clone() {
		return new Bin({
			client: this.client,
			bin: this.bin,
			volume: this.volume,
			pdf: this.pdf,
			cdf: this.cdf
		});
	}

	format(property) {
		switch(property) {
			case 'pdf':
			case 'cdf':
				let value = (Math.floor(this[property] * 10000) / 100).toFixed(2);
				if (value < 10) {
					value = '0' + value;
				}
				return value + '%';
			case 'volume':
				return this[property].toLocaleString();
			default:
				return this[property];
		}
	}

	getSchema() {
		return ['bin', 'volume', 'pdf', 'cdf'];
	}
}

class HistogramTable {
	constructor(id, bins, type) {
		this.table = document.getElementById(id);
		this.bins = bins;
		this.type = type;
		this.schema = bins[0].getSchema();
		this.maxPdf = Math.max.apply(null, this.bins.map(bin => bin.pdf));
	}

	draw() {
		Array.from(this.table.children).forEach(child => this.table.removeChild(child));

		const thead = el('thead');
		const headerRow = el('tr');
		this.schema.forEach(col => {
			const th = el('th');
			th.textContent = col === 'bin' ? this.type : col;
			headerRow.appendChild(th);
		});
		thead.appendChild(headerRow);
		this.table.appendChild(thead);

		const tbody = el('tbody');
		this.bins.forEach(bin => {
			tbody.appendChild(bin.toRow(this.maxPdf));
		});
		this.table.appendChild(tbody);
	}
}

let redrawHistogramTable = null;
function drawHistogramTable(data, desktopId, mobileId, type, [start, end]=[-Infinity, Infinity]) {
	if (!redrawHistogramTable) {
		// Return a curried function to redraw the table given start/end bins.
		redrawHistogramTable = debounce((dateRange) => {
			return drawHistogramTable(data, desktopId, mobileId, type, dateRange);
		}, 100);
	}

	const bins = data.filter(data => {
		return data.bin >= start && data.bin <= end
	}).map(data => new Bin(data));

	const desktop = bins.filter(data => data.client === 'desktop');
	const mobile = bins.filter(data => data.client === 'mobile');

	if (desktop.length) {
		(new HistogramTable(desktopId, desktop, type)).draw();
	}
	if (mobile.length) {
		(new HistogramTable(mobileId, mobile, type)).draw();
	}
}

function drawHistogram(data, containerId, options) {
	// Outliers must be at least this bin index, regardless of CDF.
	// This guarantees 30 bins worth of non-outlier data.
	// Important for histograms that are extremely skewed,
	// eg where the first bin is already 90+% PDF.
	const OUTLIER_MIN = 30;
	data = data.map((data) => new Bin(data));

	let outliers = null;
	let desktop = data.filter(({client}) => client=='desktop').reduce((data, current, i) => {
		if (current.cdf < 0.95 || i < OUTLIER_MIN) data.push(current);
		else if (outliers) outliers.add(current);
		else outliers = current;
		return data;
	}, []);
	const desktopOutliers = outliers && outliers.clone();

	outliers = null;
	let mobile = data.filter(({client}) => client=='mobile').reduce((data, current, i) => {
		if (current.cdf < 0.95 || i < OUTLIER_MIN) data.push(current);
		else if (outliers) outliers.add(current);
		else outliers = current;
		return data;
	}, []);

	let desktopCDF = desktop.map(data => data.toCdfPoint());
	if (desktopOutliers) {
		desktopCDF.push(desktopOutliers.toCdfPoint());
	}
	let mobileCDF = mobile.map(data => data.toCdfPoint());
	if (outliers) {
		mobileCDF.push(outliers.toCdfPoint());
	}

	const series = [];
	if (desktop.length) {
		series.push({
			data: desktop.map((data) => data.toPoint()),
			pointPadding: 0,
			groupPadding: 0,
			pointPlacement: 'between',
			name: 'Desktop',
			color: COLOR_DESKTOP
		});
		series.push({
			data: desktopCDF,
			type: 'line',
			marker: {
				enabled: false
			},
			name: 'Desktop CDF',
			color: COLOR_DESKTOP_ALT,
			yAxis: 1
		});
	}
	if (mobile.length) {
		series.push({
			data: mobile.map((data) => data.toPoint()),
			pointPadding: 0,
			groupPadding: 0,
			pointPlacement: 'between',
			name: 'Mobile',
			color: COLOR_MOBILE
		});
		series.push({
			data: mobileCDF,
			type: 'line',
			marker: {
				enabled: false
			},
			name: 'Mobile CDF',
			color: COLOR_MOBILE_ALT,
			yAxis: 1
		});
	}
	if (desktopOutliers) {
		series.push({
			data: [desktopOutliers.toPoint()],
			pointPadding: 0,
			groupPadding: 0,
			pointPlacement: 'between',
			name: 'Desktop Outliers',
			color: COLOR_DESKTOP,
			showInLegend: false
		});
	}
	if (outliers) {
		series.push({
			data: [outliers.toPoint()],
			pointPadding: 0,
			groupPadding: 0,
			pointPlacement: 'between',
			name: 'Mobile Outliers',
			color: COLOR_MOBILE,
			showInLegend: false
		});
	}

	drawChart(series, containerId, options);
};

function drawChart(series, containerId, options) {
	const chart = Highcharts.chart(containerId, {
<<<<<<>>>>>> babf8c52e3dd5590f5477db0cb76551ee30e27e3
		chart: {
			type: 'column',
				zoomType: 'x',
				resetZoomButton: {
				position: {
					x: 0,
					y: -50
				}
			},
		},
	  title: {
	      text: `Histogram of ${options.name}`
	  },
	  subtitle: {
	      text: `Source: <a href="http://httparchive.org">httparchive.org</a> (${prettyDate(options.date)})`,
	      useHTML: true
	  },
	  plotOptions: {
	    column: {
	      grouping: false
	    },
	    series: {
	    	events: {
	    		// Keep outlier visibility in sync.
	    		hide: function() {
	    			const outliers = chart.series.find(s => s.name === `${this.name} Outliers`);
	    			if (outliers) {
	    				outliers.hide();
	    			}

	    		},
	    		show: function() {
	    			const outliers = chart.series.find(s => s.name === `${this.name} Outliers`);
	    			if (outliers) {
	    				outliers.show();
	    			}
	    		}
	    	}
	    }
	  },
	  tooltip: {
	      formatter: function() {
	        const tooltips = [];
	        this.points.forEach(point => {
	          tooltips.push(`<span style="color: ${point.color};">■</span> <b>${point.series.name}:</b> ${Math.round(point.x * 100) / 100} ${options.type} (${(point.y).toFixed(2)})`);
	        });
	        return tooltips.join('
');
	      },
	      shared: true
	  },
	  xAxis: {
	      title: {
	          text: options.type
	      },
		  events: {
			setExtremes: e => redrawHistogramTable([e.min || -Infinity, e.max || Infinity])
		  }
	  },
	  yAxis: [{
	      title: {
	          text: 'Volume'
	      }
	  }, {
	    title: {
	      text: 'CDF (%)'
	    },
	    max: 100,
	    opposite: true
	  }],
	  series,
	  credits: false,
	  exporting: chartExportOptions
	});
}

// Export directly to global scope for use by Jinja template.
window.histogram = histogram;
<commit_msg>fix merge<commit_after>import { Colors } from './colors';
import debounce from './debounce';
import { el, prettyDate, chartExportOptions } from './utils';

const [COLOR_DESKTOP, COLOR_MOBILE, COLOR_DESKTOP_ALT, COLOR_MOBILE_ALT] = Colors.getAll({rgba: true});

function histogram(metric, date, options) {
	options.date = date;
	options.metric = metric;
	const dataUrl = `https://cdn.httparchive.org/reports/${date}/${metric}.json`;
	fetch(dataUrl)
		.then(response => {
			if (!response.ok) {
				console.error('Error loading histogram data', dataUrl, response);
				return Promise.reject(response.statusText);
			}
			return response.text();
		})
		.then(jsonStr => JSON.parse(jsonStr))
		.then(data => {
			drawHistogram(data, `${metric}-chart`, options);
			drawHistogramTable(data, `${metric}-table-desktop`, `${metric}-table-mobile`, options.type);
		}).catch(e => {
			const chart = document.getElementById(`${metric}-chart`);
			chart.innerText = `Error loading data: ${e}. Try a more recent start date.`;
		});
}

class Bin {
	constructor(data) {
		this.client = data.client;
		this.bin = +data.bin;
		this.volume = +data.volume;
		this.pdf = +data.pdf;
		this.cdf = +data.cdf;
	}

	toPoint() {
		return [this.bin, this.volume];
	}

	toCdfPoint() {
		return [this.bin, this.cdf * 100];
	}

	toRow(maxPdf) {
		const tr = document.createElement('tr');
		this.getSchema().forEach(col => {
			const td = document.createElement('td');
			if (col === 'volume') {
				const bar = document.createElement('div');
				bar.style.width = (this.pdf * 100 / maxPdf) + '%';
				bar.classList.add(this.client);
				td.appendChild(bar);
				const text = document.createElement('span');
				text.textContent = this.format(col);
				td.appendChild(text);
			}
			else {
				td.textContent = this.format(col);
			}
			td.classList.add(col);
			tr.appendChild(td);
		})
		return tr;
	}

	add(bin) {
		this.volume += bin.volume;
		this.pdf += bin.pdf;
		this.cdf = Math.max(this.cdf, bin.cdf);
	}

	clone() {
		return new Bin({
			client: this.client,
			bin: this.bin,
			volume: this.volume,
			pdf: this.pdf,
			cdf: this.cdf
		});
	}

	format(property) {
		switch(property) {
			case 'pdf':
			case 'cdf':
				let value = (Math.floor(this[property] * 10000) / 100).toFixed(2);
				if (value < 10) {
					value = '0' + value;
				}
				return value + '%';
			case 'volume':
				return this[property].toLocaleString();
			default:
				return this[property];
		}
	}

	getSchema() {
		return ['bin', 'volume', 'pdf', 'cdf'];
	}
}

class HistogramTable {
	constructor(id, bins, type) {
		this.table = document.getElementById(id);
		this.bins = bins;
		this.type = type;
		this.schema = bins[0].getSchema();
		this.maxPdf = Math.max.apply(null, this.bins.map(bin => bin.pdf));
	}

	draw() {
		Array.from(this.table.children).forEach(child => this.table.removeChild(child));

		const thead = el('thead');
		const headerRow = el('tr');
		this.schema.forEach(col => {
			const th = el('th');
			th.textContent = col === 'bin' ? this.type : col;
			headerRow.appendChild(th);
		});
		thead.appendChild(headerRow);
		this.table.appendChild(thead);

		const tbody = el('tbody');
		this.bins.forEach(bin => {
			tbody.appendChild(bin.toRow(this.maxPdf));
		});
		this.table.appendChild(tbody);
	}
}

let redrawHistogramTable = null;
function drawHistogramTable(data, desktopId, mobileId, type, [start, end]=[-Infinity, Infinity]) {
	if (!redrawHistogramTable) {
		// Return a curried function to redraw the table given start/end bins.
		redrawHistogramTable = debounce((dateRange) => {
			return drawHistogramTable(data, desktopId, mobileId, type, dateRange);
		}, 100);
	}

	const bins = data.filter(data => {
		return data.bin >= start && data.bin <= end
	}).map(data => new Bin(data));

	const desktop = bins.filter(data => data.client === 'desktop');
	const mobile = bins.filter(data => data.client === 'mobile');

	if (desktop.length) {
		(new HistogramTable(desktopId, desktop, type)).draw();
	}
	if (mobile.length) {
		(new HistogramTable(mobileId, mobile, type)).draw();
	}
}

function drawHistogram(data, containerId, options) {
	// Outliers must be at least this bin index, regardless of CDF.
	// This guarantees 30 bins worth of non-outlier data.
	// Important for histograms that are extremely skewed,
	// eg where the first bin is already 90+% PDF.
	const OUTLIER_MIN = 30;
	data = data.map((data) => new Bin(data));

	let outliers = null;
	let desktop = data.filter(({client}) => client=='desktop').reduce((data, current, i) => {
		if (current.cdf < 0.95 || i < OUTLIER_MIN) data.push(current);
		else if (outliers) outliers.add(current);
		else outliers = current;
		return data;
	}, []);
	const desktopOutliers = outliers && outliers.clone();

	outliers = null;
	let mobile = data.filter(({client}) => client=='mobile').reduce((data, current, i) => {
		if (current.cdf < 0.95 || i < OUTLIER_MIN) data.push(current);
		else if (outliers) outliers.add(current);
		else outliers = current;
		return data;
	}, []);

	let desktopCDF = desktop.map(data => data.toCdfPoint());
	if (desktopOutliers) {
		desktopCDF.push(desktopOutliers.toCdfPoint());
	}
	let mobileCDF = mobile.map(data => data.toCdfPoint());
	if (outliers) {
		mobileCDF.push(outliers.toCdfPoint());
	}

	const series = [];
	if (desktop.length) {
		series.push({
			data: desktop.map((data) => data.toPoint()),
			pointPadding: 0,
			groupPadding: 0,
			pointPlacement: 'between',
			name: 'Desktop',
			color: COLOR_DESKTOP
		});
		series.push({
			data: desktopCDF,
			type: 'line',
			marker: {
				enabled: false
			},
			name: 'Desktop CDF',
			color: COLOR_DESKTOP_ALT,
			yAxis: 1
		});
	}
	if (mobile.length) {
		series.push({
			data: mobile.map((data) => data.toPoint()),
			pointPadding: 0,
			groupPadding: 0,
			pointPlacement: 'between',
			name: 'Mobile',
			color: COLOR_MOBILE
		});
		series.push({
			data: mobileCDF,
			type: 'line',
			marker: {
				enabled: false
			},
			name: 'Mobile CDF',
			color: COLOR_MOBILE_ALT,
			yAxis: 1
		});
	}
	if (desktopOutliers) {
		series.push({
			data: [desktopOutliers.toPoint()],
			pointPadding: 0,
			groupPadding: 0,
			pointPlacement: 'between',
			name: 'Desktop Outliers',
			color: COLOR_DESKTOP,
			showInLegend: false
		});
	}
	if (outliers) {
		series.push({
			data: [outliers.toPoint()],
			pointPadding: 0,
			groupPadding: 0,
			pointPlacement: 'between',
			name: 'Mobile Outliers',
			color: COLOR_MOBILE,
			showInLegend: false
		});
	}

	drawChart(series, containerId, options);
};

function drawChart(series, containerId, options) {
	const chart = Highcharts.chart(containerId, {
		metric: options.metric,
		type: 'histogram',
		date: options.date,
		chart: {
			type: 'column',
				zoomType: 'x',
				resetZoomButton: {
				position: {
					x: 0,
					y: -50
				}
			},
		},
	  title: {
	      text: `Histogram of ${options.name}`
	  },
	  subtitle: {
	      text: `Source: <a href="http://httparchive.org">httparchive.org</a> (${prettyDate(options.date)})`,
	      useHTML: true
	  },
	  plotOptions: {
	    column: {
	      grouping: false
	    },
	    series: {
	    	events: {
	    		// Keep outlier visibility in sync.
	    		hide: function() {
	    			const outliers = chart.series.find(s => s.name === `${this.name} Outliers`);
	    			if (outliers) {
	    				outliers.hide();
	    			}

	    		},
	    		show: function() {
	    			const outliers = chart.series.find(s => s.name === `${this.name} Outliers`);
	    			if (outliers) {
	    				outliers.show();
	    			}
	    		}
	    	}
	    }
	  },
	  tooltip: {
	      formatter: function() {
	        const tooltips = [];
	        this.points.forEach(point => {
	          tooltips.push(`<span style="color: ${point.color};">■</span> <b>${point.series.name}:</b> ${Math.round(point.x * 100) / 100} ${options.type} (${(point.y).toFixed(2)})`);
	        });
	        return tooltips.join('
');
	      },
	      shared: true
	  },
	  xAxis: {
	      title: {
	          text: options.type
	      },
		  events: {
			setExtremes: e => redrawHistogramTable([e.min || -Infinity, e.max || Infinity])
		  }
	  },
	  yAxis: [{
	      title: {
	          text: 'Volume'
	      }
	  }, {
	    title: {
	      text: 'CDF (%)'
	    },
	    max: 100,
	    opposite: true
	  }],
	  series,
	  credits: false,
	  exporting: chartExportOptions
	});
}

// Export directly to global scope for use by Jinja template.
window.histogram = histogram;
<|endoftext|>


<commit_before>(function () {
	'use-strict';

	angular.module('jkuri.gallery', []).directive('ngGallery', ngGallery);

	ngGallery.$inject = ['$document', '$timeout', '$q', '$templateCache'];

	function ngGallery( $document, $timeout, $q, $templateCache ) {

		var defaults = {
			baseClass: 'ng-gallery',
			thumbClass: 'ng-thumb',
			templateUrl: 'ng-gallery.html'
		};

		var keys_codes = {
			enter: 13,
			esc: 27,
			left: 37,
			right: 39
		};

		function setScopeValues(scope, attrs) {
			scope.baseClass = scope.class || defaults.baseClass;
			scope.thumbClass = scope.thumbClass || defaults.thumbClass;
			scope.thumbsNum = scope.thumbsNum || 3; // should be odd
		}

		var template_url = defaults.templateUrl;
		// Set the default template
		$templateCache.put(template_url,
			'
' +
			'
' +
			'    ' +
			'
' +
			'
' +
			'
' +
			'
' +
			'
' +
			'

' +
			'' +
			'  ' +
			'  ' +
			'  ' +
			'  ' +
			'  {{ index + 1 }}/{{ images.length }} - {{ description }}' +
			'
' +
			'
' +
			'
' +
			'        ' +
			'
' +
			'
' +
			'
' +
			'
'
		);

		return {
			restrict: 'EA',
			scope: {
				images: '=',
				thumbsNum: '@'
			},
			controller: [
				'$scope',
				function($scope) {
					$scope.$on('openGallery', function(e, args) {
						$scope.openGallery(args.index);
					});
				}
			],
			templateUrl: function (element, attrs) {
				return attrs.templateUrl || defaults.templateUrl;
			},
			link: function (scope, element, attrs) {
				setScopeValues(scope, attrs);

				if (scope.thumbsNum >= 11) {
					scope.thumbsNum = 11;
				}

				var $body = $document.find('body');
				var $thumbwrapper = angular.element(element[0].querySelectorAll('.ng-thumbnails-wrapper'));
				var $thumbnails = angular.element(element[0].querySelectorAll('.ng-thumbnails'));

				scope.index = 0;
				scope.opened = false;

				scope.thumb_wrapper_width = 0;
				scope.thumbs_width = 0;

				var loadImage = function (i) {
					var deferred = $q.defer();
					var image = new Image();

					image.onload = function () {
						scope.loading = false;
						if (typeof this.complete === false || this.naturalWidth === 0) {
							deferred.reject();
						}
						deferred.resolve(image);
					};

					image.onerror = function () {
						deferred.reject();
					};

					image.src = scope.images[i].img;
					scope.loading = true;

					return deferred.promise;
				};

				var showImage = function (i) {
					loadImage(scope.index).then(function (resp) {
						scope.img = resp.src;
						smartScroll(scope.index);
					});
					scope.description = scope.images[i].description || '';
				};

				scope.showImageDownloadButton = function () {
					if(scope.images[scope.index] == null ||  scope.images[scope.index].downloadSrc == null) return
					var image = scope.images[scope.index];
					return angular.isDefined(image.downloadSrc) && 0 < image.downloadSrc.length;
				};

				scope.getImageDownloadSrc = function () {
					if(scope.images[scope.index] == null ||  scope.images[scope.index].downloadSrc == null) return
					return scope.images[scope.index].downloadSrc;
				};

				scope.changeImage = function (i) {
					scope.index = i;
					showImage(i);
				};

				scope.nextImage = function () {
					scope.index += 1;
					if (scope.index === scope.images.length) {
						scope.index = 0;
					}
					showImage(scope.index);
				};

				scope.prevImage = function () {
					scope.index -= 1;
					if (scope.index < 0) {
						scope.index = scope.images.length - 1;
					}
					showImage(scope.index);
				};

				scope.openGallery = function (i) {
					if (typeof i !== undefined) {
						scope.index = i;
						showImage(scope.index);
					}
					scope.opened = true;

					$timeout(function () {
						var calculatedWidth = calculateThumbsWidth();
						scope.thumbs_width = calculatedWidth.width;
						$thumbnails.css({width: calculatedWidth.width + 'px'});
						$thumbwrapper.css({width: calculatedWidth.visible_width + 'px'});
						smartScroll(scope.index);
					});
				};

				scope.closeGallery = function () {
					scope.opened = false;
				};

				$body.bind('keydown', function (event) {
					if (!scope.opened) {
						return;
					}
					var which = event.which;
					if (which === keys_codes.esc) {
						scope.closeGallery();
					} else if (which === keys_codes.right || which === keys_codes.enter) {
						scope.nextImage();
					} else if (which === keys_codes.left) {
						scope.prevImage();
					}

					scope.$apply();
				});

				var calculateThumbsWidth = function () {
					var width = 0,
						visible_width = 0;
					angular.forEach($thumbnails.find('img'), function (thumb) {
						width += thumb.clientWidth;
						width += 10; // margin-right
						visible_width = thumb.clientWidth + 10;
					});
					return {
						width: width,
						visible_width: visible_width * scope.thumbsNum
					};
				};

				var smartScroll = function (index) {
					$timeout(function () {
						var len = scope.images.length,
							width = scope.thumbs_width,
							current_scroll = $thumbwrapper[0].scrollLeft,
							item_scroll = parseInt(width / len, 10),
							i = index + 1,
							s = Math.ceil(len / i);

						$thumbwrapper[0].scrollLeft = 0;
						$thumbwrapper[0].scrollLeft = i * item_scroll - (s * item_scroll);
					}, 100);
				};

			}
		};
	}
})();
<commit_msg>Remove currently unused variable current_scroll<commit_after>(function () {
	'use-strict';

	angular.module('jkuri.gallery', []).directive('ngGallery', ngGallery);

	ngGallery.$inject = ['$document', '$timeout', '$q', '$templateCache'];

	function ngGallery( $document, $timeout, $q, $templateCache ) {

		var defaults = {
			baseClass: 'ng-gallery',
			thumbClass: 'ng-thumb',
			templateUrl: 'ng-gallery.html'
		};

		var keys_codes = {
			enter: 13,
			esc: 27,
			left: 37,
			right: 39
		};

		function setScopeValues(scope, attrs) {
			scope.baseClass = scope.class || defaults.baseClass;
			scope.thumbClass = scope.thumbClass || defaults.thumbClass;
			scope.thumbsNum = scope.thumbsNum || 3; // should be odd
		}

		var template_url = defaults.templateUrl;
		// Set the default template
		$templateCache.put(template_url,
			'
' +
			'
' +
			'    ' +
			'
' +
			'
' +
			'
' +
			'
' +
			'
' +
			'

' +
			'' +
			'  ' +
			'  ' +
			'  ' +
			'  ' +
			'  {{ index + 1 }}/{{ images.length }} - {{ description }}' +
			'
' +
			'
' +
			'
' +
			'        ' +
			'
' +
			'
' +
			'
' +
			'
'
		);

		return {
			restrict: 'EA',
			scope: {
				images: '=',
				thumbsNum: '@'
			},
			controller: [
				'$scope',
				function($scope) {
					$scope.$on('openGallery', function(e, args) {
						$scope.openGallery(args.index);
					});
				}
			],
			templateUrl: function (element, attrs) {
				return attrs.templateUrl || defaults.templateUrl;
			},
			link: function (scope, element, attrs) {
				setScopeValues(scope, attrs);

				if (scope.thumbsNum >= 11) {
					scope.thumbsNum = 11;
				}

				var $body = $document.find('body');
				var $thumbwrapper = angular.element(element[0].querySelectorAll('.ng-thumbnails-wrapper'));
				var $thumbnails = angular.element(element[0].querySelectorAll('.ng-thumbnails'));

				scope.index = 0;
				scope.opened = false;

				scope.thumb_wrapper_width = 0;
				scope.thumbs_width = 0;

				var loadImage = function (i) {
					var deferred = $q.defer();
					var image = new Image();

					image.onload = function () {
						scope.loading = false;
						if (typeof this.complete === false || this.naturalWidth === 0) {
							deferred.reject();
						}
						deferred.resolve(image);
					};

					image.onerror = function () {
						deferred.reject();
					};

					image.src = scope.images[i].img;
					scope.loading = true;

					return deferred.promise;
				};

				var showImage = function (i) {
					loadImage(scope.index).then(function (resp) {
						scope.img = resp.src;
						smartScroll(scope.index);
					});
					scope.description = scope.images[i].description || '';
				};

				scope.showImageDownloadButton = function () {
					if(scope.images[scope.index] == null ||  scope.images[scope.index].downloadSrc == null) return
					var image = scope.images[scope.index];
					return angular.isDefined(image.downloadSrc) && 0 < image.downloadSrc.length;
				};

				scope.getImageDownloadSrc = function () {
					if(scope.images[scope.index] == null ||  scope.images[scope.index].downloadSrc == null) return
					return scope.images[scope.index].downloadSrc;
				};

				scope.changeImage = function (i) {
					scope.index = i;
					showImage(i);
				};

				scope.nextImage = function () {
					scope.index += 1;
					if (scope.index === scope.images.length) {
						scope.index = 0;
					}
					showImage(scope.index);
				};

				scope.prevImage = function () {
					scope.index -= 1;
					if (scope.index < 0) {
						scope.index = scope.images.length - 1;
					}
					showImage(scope.index);
				};

				scope.openGallery = function (i) {
					if (typeof i !== undefined) {
						scope.index = i;
						showImage(scope.index);
					}
					scope.opened = true;

					$timeout(function () {
						var calculatedWidth = calculateThumbsWidth();
						scope.thumbs_width = calculatedWidth.width;
						$thumbnails.css({width: calculatedWidth.width + 'px'});
						$thumbwrapper.css({width: calculatedWidth.visible_width + 'px'});
						smartScroll(scope.index);
					});
				};

				scope.closeGallery = function () {
					scope.opened = false;
				};

				$body.bind('keydown', function (event) {
					if (!scope.opened) {
						return;
					}
					var which = event.which;
					if (which === keys_codes.esc) {
						scope.closeGallery();
					} else if (which === keys_codes.right || which === keys_codes.enter) {
						scope.nextImage();
					} else if (which === keys_codes.left) {
						scope.prevImage();
					}

					scope.$apply();
				});

				var calculateThumbsWidth = function () {
					var width = 0,
						visible_width = 0;
					angular.forEach($thumbnails.find('img'), function (thumb) {
						width += thumb.clientWidth;
						width += 10; // margin-right
						visible_width = thumb.clientWidth + 10;
					});
					return {
						width: width,
						visible_width: visible_width * scope.thumbsNum
					};
				};

				var smartScroll = function (index) {
					$timeout(function () {
						var len = scope.images.length,
							width = scope.thumbs_width,
							item_scroll = parseInt(width / len, 10),
							i = index + 1,
							s = Math.ceil(len / i);

						$thumbwrapper[0].scrollLeft = 0;
						$thumbwrapper[0].scrollLeft = i * item_scroll - (s * item_scroll);
					}, 100);
				};

			}
		};
	}
})();
<|endoftext|>


<commit_before>// ------------------------------------------------------------------------
// GMaps: prototype.js
// ------------------------------------------------------------------------

!((ComponentType) => {
  "use strict"

  // ----------------------------------------------------------------------
  // Public Methods
  // ----------------------------------------------------------------------

  gmap.prototype = {

    add: function(type, compOptions) {
      return Core.addComponent({
        compOptions : compOptions,
        map         : this,
        type        : type
      })
    },

    fitBounds: function(comps) {
      return Core.fitBounds({
        map   : this,
        comps : comps
      })
    },

    getBounds: function() {
      return this.Obj.getBounds()
    },

    getCenter: function() {
      return this.Obj.getCenter()
    },

    getZoom: function() {
      return this.Obj.getZoom()
    },

    labels: function(ids) {
      return Core.search({
        ids      : ids,
        map      : this,
        matching : true,
        type     : ComponentType.LABEL
      })
    },

    markers: function(ids) {
      return Core.search({
        ids      : ids,
        map      : this,
        matching : true,
        type     : ComponentType.MARKER
      })
    },

    off: function(type) {
      return Core.removeListener({
        compArray : this,
        type      : type
      })
    },

    on: function(type, func) {
      return Core.addListener({
        compArray : this,
        func      : func,
        type      : type
      })
    },

    polygons: function(ids) {
      return Core.search({
        ids      : ids,
        map      : this,
        matching : true,
        type     : ComponentType.POLYGON
      })
    },

    remove: function(type, ids) {
      return Core.remove({
        ids  : ids,
        map  : this,
        type : type
      })
    },

    reset: function() {
      return Core.reset({ compArray: this })
    },

    setCenter: function(center) {
      if (center !== undefined) {
        this.Obj.setCenter(Util.toLatLng(center))
      }
      return this
    },

    setZoom: function(zoom) {
      if (zoom !== undefined) {
        this.Obj.setZoom(zoom)
      }
      return this
    },

    update: function(compOptions) {
      return Core.update({
        compArray   : this,
        compOptions : compOptions
      })
    }

  }

  return gmap
})(Const.ComponentType)
<commit_msg>added remaining native map methods<commit_after>// ------------------------------------------------------------------------
// GMaps: prototype.js
// ------------------------------------------------------------------------

!((ComponentType) => {
  "use strict"

  // ----------------------------------------------------------------------
  // Public Methods
  // ----------------------------------------------------------------------

  gmap.prototype = {

    add: function(type, compOptions) {
      return Core.addComponent({
        compOptions : compOptions,
        map         : this,
        type        : type
      })
    },

    fitBounds: function(comps) {
      return Core.fitBounds({
        map   : this,
        comps : comps
      })
    },

    getBounds: function() {
      return this.Obj.getBounds()
    },

    getCenter: function() {
      return this.Obj.getCenter()
    },

    getClickableIcons: function() {
      return this.Obj.getClickableIcons()
    },

    getDiv: function() {
      return this.Obj.getDiv()
    },

    getMapTypeId: function() {
      return this.Obj.getMapTypeId()
    },

    getProjection: function() {
      return this.Obj.getProjection()
    },

    getStreetView: function() {
      return this.Obj.getStreetView()
    },

    getZoom: function() {
      return this.Obj.getZoom()
    },

    labels: function(ids) {
      return Core.search({
        ids      : ids,
        map      : this,
        matching : true,
        type     : ComponentType.LABEL
      })
    },

    markers: function(ids) {
      return Core.search({
        ids      : ids,
        map      : this,
        matching : true,
        type     : ComponentType.MARKER
      })
    },

    off: function(type) {
      return Core.removeListener({
        compArray : this,
        type      : type
      })
    },

    on: function(type, func) {
      return Core.addListener({
        compArray : this,
        func      : func,
        type      : type
      })
    },

    polygons: function(ids) {
      return Core.search({
        ids      : ids,
        map      : this,
        matching : true,
        type     : ComponentType.POLYGON
      })
    },

    remove: function(type, ids) {
      return Core.remove({
        ids  : ids,
        map  : this,
        type : type
      })
    },

    reset: function() {
      return Core.reset({ compArray: this })
    },

    setCenter: function(center) {
      if (center !== undefined) {
        this.Obj.setCenter(Util.toLatLng(center))
      }
      return this
    },

    setClickableIcons: function(bool) {
      this.Obj.setClickableIcons(bool)
      return this
    },

    setMapTypeId: function(mapTypeId) {
      this.Obj.setMapTypeId(mapTypeId)
      return this
    },

    setStreetView: function(streetView) {
      this.Obj.setStreetView(streetView)
      return this
    },

    setZoom: function(zoom) {
      if (zoom !== undefined) {
        this.Obj.setZoom(zoom)
      }
      return this
    },

    update: function(compOptions) {
      return Core.update({
        compArray   : this,
        compOptions : compOptions
      })
    }

  }

  return gmap
})(Const.ComponentType)
<|endoftext|>


<commit_before>  function isNumber(n) {
    return typeof n === 'number' && !isNaN(n);
  }

  function isUndefined(n) {
    return typeof n === 'undefined';
  }

  function toArray(obj, offset) {
    var args = [];

    // This is necessary for IE8
    if (isNumber(offset)) {
      args.push(offset);
    }

    return args.slice.apply(obj, args);
  }

  // Custom proxy to avoid jQuery's guid
  function proxy(fn, context) {
    var args = toArray(arguments, 2);

    return function () {
      return fn.apply(context, args.concat(toArray(arguments)));
    };
  }

  function isCrossOriginURL(url) {
    var parts = url.match(/^(https?:)\/\/([^\:\/\?#]+):?(\d*)/i);

    return parts && (
      parts[1] !== location.protocol ||
      parts[2] !== location.hostname ||
      parts[3] !== location.port
    );
  }

  function addTimestamp(url) {
    var timestamp = 'timestamp=' + (new Date()).getTime();

    return (url + (url.indexOf('?') === -1 ? '?' : '&') + timestamp);
  }

  function getTransform(options) {
    var transforms = [];
    var rotate = options.rotate;
    var scaleX = options.scaleX;
    var scaleY = options.scaleY;

    if (isNumber(rotate)) {
      transforms.push('rotate(' + rotate + 'deg)');
    }

    if (isNumber(scaleX) && isNumber(scaleY)) {
      transforms.push('scale(' + scaleX + ',' + scaleY + ')');
    }

    return transforms.length ? transforms.join(' ') : 'none';
  }

  function getRotatedSizes(data, reverse) {
    var deg = abs(data.degree) % 180;
    var arc = (deg > 90 ? (180 - deg) : deg) * Math.PI / 180;
    var sinArc = sin(arc);
    var cosArc = cos(arc);
    var width = data.width;
    var height = data.height;
    var aspectRatio = data.aspectRatio;
    var newWidth;
    var newHeight;

    if (!reverse) {
      newWidth = width * cosArc + height * sinArc;
      newHeight = width * sinArc + height * cosArc;
    } else {
      newWidth = width / (cosArc + sinArc / aspectRatio);
      newHeight = newWidth / aspectRatio;
    }

    return {
      width: newWidth,
      height: newHeight
    };
  }

  function getSourceCanvas(image, data) {
    var canvas = $('<canvas>')[0];
    var context = canvas.getContext('2d');
    var x = 0;
    var y = 0;
    var width = data.naturalWidth;
    var height = data.naturalHeight;
    var rotate = data.rotate;
    var scaleX = data.scaleX;
    var scaleY = data.scaleY;
    var scalable = isNumber(scaleX) && isNumber(scaleY) && (scaleX !== 1 || scaleY !== 1);
    var rotatable = isNumber(rotate) && rotate !== 0;
    var advanced = rotatable || scalable;
    var canvasWidth = width;
    var canvasHeight = height;
    var translateX;
    var translateY;
    var rotated;

    if (scalable) {
      translateX = width / 2;
      translateY = height / 2;
    }

    if (rotatable) {
      rotated = getRotatedSizes({
        width: width,
        height: height,
        degree: rotate
      });

      canvasWidth = rotated.width;
      canvasHeight = rotated.height;
      translateX = rotated.width / 2;
      translateY = rotated.height / 2;
    }

    canvas.width = canvasWidth;
    canvas.height = canvasHeight;

    if (advanced) {
      x = -width / 2;
      y = -height / 2;

      context.save();
      context.translate(translateX, translateY);
    }

    if (rotatable) {
      context.rotate(rotate * Math.PI / 180);
    }

    // Should scale should after rotated
    if (scalable) {
      context.scale(scaleX, scaleY);
    }

    context.drawImage(image, x, y, width, height);

    if (advanced) {
      context.restore();
    }

    return canvas;
  }
<commit_msg>Fix typo<commit_after>  function isNumber(n) {
    return typeof n === 'number' && !isNaN(n);
  }

  function isUndefined(n) {
    return typeof n === 'undefined';
  }

  function toArray(obj, offset) {
    var args = [];

    // This is necessary for IE8
    if (isNumber(offset)) {
      args.push(offset);
    }

    return args.slice.apply(obj, args);
  }

  // Custom proxy to avoid jQuery's guid
  function proxy(fn, context) {
    var args = toArray(arguments, 2);

    return function () {
      return fn.apply(context, args.concat(toArray(arguments)));
    };
  }

  function isCrossOriginURL(url) {
    var parts = url.match(/^(https?:)\/\/([^\:\/\?#]+):?(\d*)/i);

    return parts && (
      parts[1] !== location.protocol ||
      parts[2] !== location.hostname ||
      parts[3] !== location.port
    );
  }

  function addTimestamp(url) {
    var timestamp = 'timestamp=' + (new Date()).getTime();

    return (url + (url.indexOf('?') === -1 ? '?' : '&') + timestamp);
  }

  function getTransform(options) {
    var transforms = [];
    var rotate = options.rotate;
    var scaleX = options.scaleX;
    var scaleY = options.scaleY;

    if (isNumber(rotate)) {
      transforms.push('rotate(' + rotate + 'deg)');
    }

    if (isNumber(scaleX) && isNumber(scaleY)) {
      transforms.push('scale(' + scaleX + ',' + scaleY + ')');
    }

    return transforms.length ? transforms.join(' ') : 'none';
  }

  function getRotatedSizes(data, reverse) {
    var deg = abs(data.degree) % 180;
    var arc = (deg > 90 ? (180 - deg) : deg) * Math.PI / 180;
    var sinArc = sin(arc);
    var cosArc = cos(arc);
    var width = data.width;
    var height = data.height;
    var aspectRatio = data.aspectRatio;
    var newWidth;
    var newHeight;

    if (!reverse) {
      newWidth = width * cosArc + height * sinArc;
      newHeight = width * sinArc + height * cosArc;
    } else {
      newWidth = width / (cosArc + sinArc / aspectRatio);
      newHeight = newWidth / aspectRatio;
    }

    return {
      width: newWidth,
      height: newHeight
    };
  }

  function getSourceCanvas(image, data) {
    var canvas = $('<canvas>')[0];
    var context = canvas.getContext('2d');
    var x = 0;
    var y = 0;
    var width = data.naturalWidth;
    var height = data.naturalHeight;
    var rotate = data.rotate;
    var scaleX = data.scaleX;
    var scaleY = data.scaleY;
    var scalable = isNumber(scaleX) && isNumber(scaleY) && (scaleX !== 1 || scaleY !== 1);
    var rotatable = isNumber(rotate) && rotate !== 0;
    var advanced = rotatable || scalable;
    var canvasWidth = width;
    var canvasHeight = height;
    var translateX;
    var translateY;
    var rotated;

    if (scalable) {
      translateX = width / 2;
      translateY = height / 2;
    }

    if (rotatable) {
      rotated = getRotatedSizes({
        width: width,
        height: height,
        degree: rotate
      });

      canvasWidth = rotated.width;
      canvasHeight = rotated.height;
      translateX = rotated.width / 2;
      translateY = rotated.height / 2;
    }

    canvas.width = canvasWidth;
    canvas.height = canvasHeight;

    if (advanced) {
      x = -width / 2;
      y = -height / 2;

      context.save();
      context.translate(translateX, translateY);
    }

    if (rotatable) {
      context.rotate(rotate * Math.PI / 180);
    }

    // Should call `scale` after rotated
    if (scalable) {
      context.scale(scaleX, scaleY);
    }

    context.drawImage(image, x, y, width, height);

    if (advanced) {
      context.restore();
    }

    return canvas;
  }
<|endoftext|>


<commit_before>// [VexFlow](http://vexflow.com) - Copyright (c) Mohit Muthanna 2010.
// Author: Cyril Silverman
//
// ## Description
//
// This file implements key signatures. A key signature sits on a stave
// and indicates the notes with implicit accidentals.
Vex.Flow.KeySignature = (function() {
  function KeySignature(keySpec) {
    if (arguments.length > 0) this.init(keySpec);
  }

  // Space between natural and following accidental depending
  // on vertical position
  KeySignature.accidentalSpacing = {
    '#': {
      above: 6,
      below: 4
    },
    'b': {
      above: 4,
      below: 7
    },
    'n': {
      above: 3,
      below: -1
    }
  };

  // ## Prototype Methods
  Vex.Inherit(KeySignature, Vex.Flow.StaveModifier, {
    // Create a new Key Signature based on a `key_spec`
    init: function(key_spec) {
      KeySignature.superclass.init();

      this.glyphFontScale = 38; // TODO(0xFE): Should this match StaveNote?
      this.accList = Vex.Flow.keySignature(key_spec);
    },

    // Add an accidental glyph to the `stave`. `acc` is the data of the
    // accidental to add. If the `next` accidental is also provided, extra
    // width will be added to the initial accidental for optimal spacing.
    addAccToStave: function(stave, acc, next) {
      var glyph_data = Vex.Flow.accidentalCodes(acc.type);
      var glyph = new Vex.Flow.Glyph(glyph_data.code, this.glyphFontScale);

      // Determine spacing between current accidental and the next accidental
      var extra_width = 0;
      if (acc.type === "n" && next) {
        var above = next.line >= acc.line;
        var space = KeySignature.accidentalSpacing[next.type];
        extra_width = above ? space.above : space.below;
      }

      // Set the width and place the glyph on the stave
      glyph.setWidth(glyph_data.width + extra_width);
      this.placeGlyphOnLine(glyph, stave, acc.line);
      stave.addGlyph(glyph);
    },

    // Cancel out a key signature provided in the `spec` parameter. This will
    // place appropriate natural accidentals before the key signature.
    cancelKey: function(spec) {
      // Get the accidental list for the cancelled key signature
      var cancel_accList = Vex.Flow.keySignature(spec);

      // If the cancelled key has a different accidental type, ie: # vs b
      var different_types = this.accList.length > 0 &&
                            cancel_accList[0].type !== this.accList[0].type;

      // Determine how many naturals needed to add
      var naturals = 0;
      if (different_types) {
        naturals = cancel_accList.length;
      } else {
        naturals = cancel_accList.length - this.accList.length;
      }

      // Return if no naturals needed
      if (naturals < 1) return;

      // Get the line position for each natural
      var cancelled = [];
      for (var i = 0; i < naturals; i++) {
        var index = i;
        if (!different_types) {
          index = cancel_accList.length - naturals + i;
        }

        var acc = cancel_accList[index];
        cancelled.push({type: "n", line: acc.line});
      }

      // Combine naturals with main accidental list for the key signature
      this.accList = cancelled.concat(this.accList);

      return this;
    },

    // Add the key signature to the `stave`. You probably want to use the
    // helper method `.addToStave()` instead
    addModifier: function(stave) {
      this.convertAccLines(stave.clef, this.accList[0].type);
      for (var i = 0; i < this.accList.length; ++i) {
        this.addAccToStave(stave, this.accList[i], this.accList[i+1]);
      }
    },

    // Add the key signature to the `stave`, if it's the not the `firstGlyph`
    // a spacer will be added as well.
    addToStave: function(stave, firstGlyph) {
      if (this.accList.length === 0)
        return this;

      if (!firstGlyph) {
        stave.addGlyph(this.makeSpacer(this.padding));
      }

      this.addModifier(stave);
      return this;
    },

    // Apply the accidental staff line placement based on the `clef` and
    // the  accidental `type` for the key signature ('# or 'b').
    convertAccLines: function(clef, type) {
      var offset = 0.0; // if clef === "treble"
      var tenorSharps;
      var isTenorSharps = ((clef === "tenor") && (type === "#")) ? true : false;

      switch (clef) {
        case "bass":
          offset = 1;
          break;
        case "alto":
          offset = 0.5;
          break;
        case "tenor":
          if (!isTenorSharps) {
            offset = -0.5;
          }
          break;
      }

      // Special-case for TenorSharps
      var i;
      if (isTenorSharps) {
        tenorSharps = [3, 1, 2.5, 0.5, 2, 0, 1.5];
        for (i = 0; i < this.accList.length; ++i) {
          this.accList[i].line = tenorSharps[i];
        }
      }
      else {
        if (clef != "treble") {
          for (i = 0; i < this.accList.length; ++i) {
            this.accList[i].line += offset;
          }
        }
      }
    }
  });

  return KeySignature;
}());<commit_msg>Add support for rendering key signatures in all clefs<commit_after>// [VexFlow](http://vexflow.com) - Copyright (c) Mohit Muthanna 2010.
// Author: Cyril Silverman
//
// ## Description
//
// This file implements key signatures. A key signature sits on a stave
// and indicates the notes with implicit accidentals.
Vex.Flow.KeySignature = (function() {
  function KeySignature(keySpec) {
    if (arguments.length > 0) this.init(keySpec);
  }

  // Space between natural and following accidental depending
  // on vertical position
  KeySignature.accidentalSpacing = {
    '#': {
      above: 6,
      below: 4
    },
    'b': {
      above: 4,
      below: 7
    },
    'n': {
      above: 3,
      below: -1
    }
  };

  // ## Prototype Methods
  Vex.Inherit(KeySignature, Vex.Flow.StaveModifier, {
    // Create a new Key Signature based on a `key_spec`
    init: function(key_spec) {
      KeySignature.superclass.init();

      this.glyphFontScale = 38; // TODO(0xFE): Should this match StaveNote?
      this.accList = Vex.Flow.keySignature(key_spec);
    },

    // Add an accidental glyph to the `stave`. `acc` is the data of the
    // accidental to add. If the `next` accidental is also provided, extra
    // width will be added to the initial accidental for optimal spacing.
    addAccToStave: function(stave, acc, next) {
      var glyph_data = Vex.Flow.accidentalCodes(acc.type);
      var glyph = new Vex.Flow.Glyph(glyph_data.code, this.glyphFontScale);

      // Determine spacing between current accidental and the next accidental
      var extra_width = 0;
      if (acc.type === "n" && next) {
        var above = next.line >= acc.line;
        var space = KeySignature.accidentalSpacing[next.type];
        extra_width = above ? space.above : space.below;
      }

      // Set the width and place the glyph on the stave
      glyph.setWidth(glyph_data.width + extra_width);
      this.placeGlyphOnLine(glyph, stave, acc.line);
      stave.addGlyph(glyph);
    },

    // Cancel out a key signature provided in the `spec` parameter. This will
    // place appropriate natural accidentals before the key signature.
    cancelKey: function(spec) {
      // Get the accidental list for the cancelled key signature
      var cancel_accList = Vex.Flow.keySignature(spec);

      // If the cancelled key has a different accidental type, ie: # vs b
      var different_types = this.accList.length > 0 &&
                            cancel_accList[0].type !== this.accList[0].type;

      // Determine how many naturals needed to add
      var naturals = 0;
      if (different_types) {
        naturals = cancel_accList.length;
      } else {
        naturals = cancel_accList.length - this.accList.length;
      }

      // Return if no naturals needed
      if (naturals < 1) return;

      // Get the line position for each natural
      var cancelled = [];
      for (var i = 0; i < naturals; i++) {
        var index = i;
        if (!different_types) {
          index = cancel_accList.length - naturals + i;
        }

        var acc = cancel_accList[index];
        cancelled.push({type: "n", line: acc.line});
      }

      // Combine naturals with main accidental list for the key signature
      this.accList = cancelled.concat(this.accList);

      return this;
    },

    // Add the key signature to the `stave`. You probably want to use the
    // helper method `.addToStave()` instead
    addModifier: function(stave) {
      this.convertAccLines(stave.clef, this.accList[0].type);
      for (var i = 0; i < this.accList.length; ++i) {
        this.addAccToStave(stave, this.accList[i], this.accList[i+1]);
      }
    },

    // Add the key signature to the `stave`, if it's the not the `firstGlyph`
    // a spacer will be added as well.
    addToStave: function(stave, firstGlyph) {
      if (this.accList.length === 0)
        return this;

      if (!firstGlyph) {
        stave.addGlyph(this.makeSpacer(this.padding));
      }

      this.addModifier(stave);
      return this;
    },

    // Apply the accidental staff line placement based on the `clef` and
    // the  accidental `type` for the key signature ('# or 'b').
    convertAccLines: function(clef, type) {
      var offset = 0.0; // if clef === "treble"
      var customLines; // when clef doesn't follow treble key sig shape

      switch (clef) {
        // Treble & Subbass both have offsets of 0, so are not included.
        case "soprano":
          if(type === "#") customLines = [2.5,0.5,2,0,1.5,-0.5,1];
          else offset = -1;
          break;
        case "mezzo-soprano":
          if(type === "b") customLines = [0,2,0.5,2.5,1,3,1.5];
          else offset = 1.5;
          break;
        case "alto":
          offset = 0.5;
          break;
        case "tenor":
          if(type === "#") customLines = [3, 1, 2.5, 0.5, 2, 0, 1.5];
          else offset = -0.5;
          break;
        case "baritone-f":
        case "baritone-c":
          if(type === "b") customLines = [0.5,2.5,1,3,1.5,3.5,2];
          else offset = 2;
          break;
        case "bass":
        case "french":
          offset = 1;
          break;
      }

      // If there's a special case, assign those lines/spaces:
      var i;
      if (typeof customLines !== "undefined") {
        for (i = 0; i < this.accList.length; ++i) {
          this.accList[i].line = customLines[i];
        }
      } else if (offset !== 0) {
        for (i = 0; i < this.accList.length; ++i) {
          this.accList[i].line += offset;
        }
      }
    }
  });

  return KeySignature;
}());<|endoftext|>


<commit_before>/*global console, jQuery, Kinetic*/
(function () {
	'use strict';
	/*shamelessly copied from http://james.padolsey.com/javascript/wordwrap-for-javascript */
	var COLUMN_WORD_WRAP_LIMIT = 25;
	function wordWrap(str, width, brk, cut) {
		brk = brk || '\n';
		width = width || 75;
		cut = cut || false;
		if (!str) {
			return str;
		}
		var regex = '.{1,' + width + '}(\\s|$)' + (cut ? '|.{' + width + '}|.+$' : '|\\S+?(\\s|$)');
		return str.match(new RegExp(regex, 'g')).join(brk);
	}
	function joinLines(string) {
		return string.replace(/\n/g, ' ');
	}
	function breakWords(string) {
		return wordWrap(joinLines(string), COLUMN_WORD_WRAP_LIMIT, '\n', false);
	}
	Kinetic.Idea = function (config) {
		var ENTER_KEY_CODE = 13,
			ESC_KEY_CODE = 27,
			self = this,
			setStageDraggable = function (isDraggable) {
				self.getStage().setDraggable(isDraggable);
			};
		config.text = breakWords(config.text);
		this.level = config.level;
		this.isSelected = false;
		this.setStyle(config);
		config.align = 'center';
		config.shadow = {
			color: 'black',
			blur: 10,
			offset: [4, 4],
			opacity: 0.4
		};
		config.cornerRadius = 6;
		config.draggable = true;
		config.name = 'Idea';
		Kinetic.Text.apply(this, [config]);
		this.classType = 'Idea';
		this.on('dblclick', self.fire.bind(self, ':nodeEditRequested'));
		this.on('mouseover touchstart', setStageDraggable.bind(null, false));
		this.on('mouseout touchend', setStageDraggable.bind(null, true));
		this.editNode = function () {
			//this only works for solid color nodes
			self.attrs.textFill = self.attrs.fill;
			self.getLayer().draw();
			var canvasPosition = jQuery(self.getLayer().getCanvas().getElement()).offset(),
				currentText = self.getText(),
				ideaInput,
				updateText = function (newText) {
					self.setStyle(self.attrs);
					self.getStage().draw();
					self.fire(':textChanged', {
						text: breakWords(newText || currentText)
					});
					ideaInput.remove();
				},
				onCommit = function () {
					updateText(ideaInput.val());
				};
			ideaInput = jQuery('<textarea type="text" class="ideaInput"></textarea>')
				.css({
					top: canvasPosition.top + self.getAbsolutePosition().y,
					left: canvasPosition.left + self.getAbsolutePosition().x,
					width: self.getWidth(),
					height: self.getHeight()
				})
				.val(joinLines(currentText))
				.appendTo('body')
				.keydown(function (e) {
					if (e.which === ENTER_KEY_CODE) {
						onCommit();
					} else if (e.which === ESC_KEY_CODE) {
						updateText(currentText);
					}
					e.stopPropagation();
				})
				.blur(onCommit)
				.focus();
		};
	};
}());
Kinetic.Idea.prototype.setStyle = function (config) {
	'use strict';
	var isDroppable = this.isDroppable,
		isSelected = this.isSelected,
		isRoot = this.level === 1;
	config.strokeWidth = 1;
	config.padding = 8;
	config.fontSize = 10;
	config.fontFamily = 'Helvetica';
	config.fontStyle = 'bold';
	if (isDroppable) {
		config.stroke = '#9F4F4F';
		config.fill = {
			start: { x: 0, y: 0 },
			end: {x: 0, y: 20 },
			colorStops: [0, '#EF6F6F', 1, '#CF4F4F']
		};
		config.textFill = '#FFFFFF';
	} else if (isSelected) {
		config.fill = '#5FBF5F';
		config.textFill = '#FFFFFF';
	} else {
		config.stroke = isRoot ? '#88F' : '#888';
		config.fill = {
			start: { x: 0, y: 0 },
			end: {x: 20, y: 40 },
			colorStops: isRoot ? [0, '#4FDFFF', 1, '#30C0FF'] : [0, '#FFFFFF', 1, '#E0E0E0']
		};
		config.textFill = isRoot ? '#FFFFFF' : '#5F5F5F';
	}
};
Kinetic.Idea.prototype.setIsSelected = function (isSelected) {
	'use strict';
	this.isSelected = isSelected;
	this.setStyle(this.attrs);
	this.getLayer().draw();
};
Kinetic.Idea.prototype.setIsDroppable = function (isDroppable) {
	'use strict';
	this.isDroppable = isDroppable;
	this.setStyle(this.attrs);
};
Kinetic.Idea.prototype.transitionToAndDontStopCurrentTransitions = function (config) {
	'use strict';
	var transition = new Kinetic.Transition(this, config),
		animation = new Kinetic.Animation();
	animation.func = transition._onEnterFrame.bind(transition);
	animation.node = this.getLayer();
	transition.onFinished = animation.stop.bind(animation);
	transition.start();
	animation.start();
};
Kinetic.Global.extend(Kinetic.Idea, Kinetic.Text);<commit_msg>line height<commit_after>/*global console, jQuery, Kinetic*/
(function () {
	'use strict';
	/*shamelessly copied from http://james.padolsey.com/javascript/wordwrap-for-javascript */
	var COLUMN_WORD_WRAP_LIMIT = 25;
	function wordWrap(str, width, brk, cut) {
		brk = brk || '\n';
		width = width || 75;
		cut = cut || false;
		if (!str) {
			return str;
		}
		var regex = '.{1,' + width + '}(\\s|$)' + (cut ? '|.{' + width + '}|.+$' : '|\\S+?(\\s|$)');
		return str.match(new RegExp(regex, 'g')).join(brk);
	}
	function joinLines(string) {
		return string.replace(/\n/g, ' ');
	}
	function breakWords(string) {
		return wordWrap(joinLines(string), COLUMN_WORD_WRAP_LIMIT, '\n', false);
	}
	Kinetic.Idea = function (config) {
		var ENTER_KEY_CODE = 13,
			ESC_KEY_CODE = 27,
			self = this,
			setStageDraggable = function (isDraggable) {
				self.getStage().setDraggable(isDraggable);
			};
		config.text = breakWords(config.text);
		this.level = config.level;
		this.isSelected = false;
		this.setStyle(config);
		config.align = 'center';
		config.shadow = {
			color: 'black',
			blur: 10,
			offset: [4, 4],
			opacity: 0.4
		};
		config.cornerRadius = 6;
		config.draggable = true;
		config.name = 'Idea';
		Kinetic.Text.apply(this, [config]);
		this.classType = 'Idea';
		this.on('dblclick', self.fire.bind(self, ':nodeEditRequested'));
		this.on('mouseover touchstart', setStageDraggable.bind(null, false));
		this.on('mouseout touchend', setStageDraggable.bind(null, true));
		this.editNode = function () {
			//this only works for solid color nodes
			self.attrs.textFill = self.attrs.fill;
			self.getLayer().draw();
			var canvasPosition = jQuery(self.getLayer().getCanvas().getElement()).offset(),
				currentText = self.getText(),
				ideaInput,
				updateText = function (newText) {
					self.setStyle(self.attrs);
					self.getStage().draw();
					self.fire(':textChanged', {
						text: breakWords(newText || currentText)
					});
					ideaInput.remove();
				},
				onCommit = function () {
					updateText(ideaInput.val());
				};
			ideaInput = jQuery('<textarea type="text" class="ideaInput"></textarea>')
				.css({
					top: canvasPosition.top + self.getAbsolutePosition().y,
					left: canvasPosition.left + self.getAbsolutePosition().x,
					width: self.getWidth(),
					height: self.getHeight()
				})
				.val(joinLines(currentText))
				.appendTo('body')
				.keydown(function (e) {
					if (e.which === ENTER_KEY_CODE) {
						onCommit();
					} else if (e.which === ESC_KEY_CODE) {
						updateText(currentText);
					}
					e.stopPropagation();
				})
				.blur(onCommit)
				.focus();
		};
	};
}());
Kinetic.Idea.prototype.setStyle = function (config) {
	'use strict';
	var isDroppable = this.isDroppable,
		isSelected = this.isSelected,
		isRoot = this.level === 1;
	config.strokeWidth = 1;
	config.padding = 8;
	config.fontSize = 10;
	config.fontFamily = 'Helvetica';
	config.lineHeight = 1.5;
	config.fontStyle = 'bold';
	if (isDroppable) {
		config.stroke = '#9F4F4F';
		config.fill = {
			start: { x: 0, y: 0 },
			end: {x: 0, y: 20 },
			colorStops: [0, '#EF6F6F', 1, '#CF4F4F']
		};
		config.textFill = '#FFFFFF';
	} else if (isSelected) {
		config.fill = '#5FBF5F';
		config.textFill = '#FFFFFF';
	} else {
		config.stroke = isRoot ? '#88F' : '#888';
		config.fill = {
			start: { x: 0, y: 0 },
			end: {x: 20, y: 40 },
			colorStops: isRoot ? [0, '#4FDFFF', 1, '#30C0FF'] : [0, '#FFFFFF', 1, '#E0E0E0']
		};
		config.textFill = isRoot ? '#FFFFFF' : '#5F5F5F';
	}
};
Kinetic.Idea.prototype.setIsSelected = function (isSelected) {
	'use strict';
	this.isSelected = isSelected;
	this.setStyle(this.attrs);
	this.getLayer().draw();
};
Kinetic.Idea.prototype.setIsDroppable = function (isDroppable) {
	'use strict';
	this.isDroppable = isDroppable;
	this.setStyle(this.attrs);
};
Kinetic.Idea.prototype.transitionToAndDontStopCurrentTransitions = function (config) {
	'use strict';
	var transition = new Kinetic.Transition(this, config),
		animation = new Kinetic.Animation();
	animation.func = transition._onEnterFrame.bind(transition);
	animation.node = this.getLayer();
	transition.onFinished = animation.stop.bind(animation);
	transition.start();
	animation.start();
};
Kinetic.Global.extend(Kinetic.Idea, Kinetic.Text);<|endoftext|>


<commit_before>import 'es6-promise';
import webstomp from 'webstomp-client';
const SockJS = require('sockjs-client');
import 'whatwg-fetch';
import {User} from './user';

function testAsync() {
    return new Promise(resolve => {
        setTimeout(() => {
            const date = new Date();
            let seconds = date.getSeconds();
            let minutes = date.getMinutes();

            seconds = seconds < 10 ? `0${ seconds }` : seconds;
            minutes = minutes < 10 ? `0${ minutes }` : minutes;

            resolve(`Current time: ${ date.getHours() }:${ minutes }:${ seconds }`);
        }, (Math.random() * 1000) + 1000); // 1-2 seconds delay
    });
}

function getUser(username) {
    return fetch("/api/user", {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({
            username: username,
        }),
    });
}

function getUsers() {
    return fetch("/api/user");
}

function getChatRoom(username, target) {
    return fetch("/api/chat?username=" + username + "&partner=" + target, {
        method: 'GET',
    })
}

const client = webstomp.over(new SockJS("http://localhost:8080/messages"), {heartbeat: false});

export default {
    testAsync,
    getUser,
    getChatRoom,
    getUsers,
    client: client,
    subscribe: client.subscribe,
    send: client.send,
};
<commit_msg>fixing relative url.<commit_after>import 'es6-promise';
import webstomp from 'webstomp-client';
const SockJS = require('sockjs-client');
import 'whatwg-fetch';
import {User} from './user';

function testAsync() {
    return new Promise(resolve => {
        setTimeout(() => {
            const date = new Date();
            let seconds = date.getSeconds();
            let minutes = date.getMinutes();

            seconds = seconds < 10 ? `0${ seconds }` : seconds;
            minutes = minutes < 10 ? `0${ minutes }` : minutes;

            resolve(`Current time: ${ date.getHours() }:${ minutes }:${ seconds }`);
        }, (Math.random() * 1000) + 1000); // 1-2 seconds delay
    });
}

function getUser(username) {
    return fetch("/api/user", {
        method: 'POST',
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify({
            username: username,
        }),
    });
}

function getUsers() {
    return fetch("/api/user");
}

function getChatRoom(username, target) {
    return fetch("/api/chat?username=" + username + "&partner=" + target, {
        method: 'GET',
    })
}

const client = webstomp.over(new SockJS("/messages"), {heartbeat: false});

export default {
    testAsync,
    getUser,
    getChatRoom,
    getUsers,
    client: client,
    subscribe: client.subscribe,
    send: client.send,
};
<|endoftext|>


<commit_before>import {isObject, isString, isArray} from "../../util.js";
import {each, values as objectValues} from "../../util.js";
import {propTypes, parseProps} from "../../props.js";
import {getValueSources} from "../../runtime/value.js";
import {getExpressionSources} from "../../runtime/expression.js";

import {setStyle, isStyleName} from "../style.js";
import {setTooltipEvents} from "../tooltip.js";
import {setEvent, isEventName} from "../events.js";

import {textGeom} from "./text.js";
import {lineGeom} from "./line.js";
import {rectangleGeom} from "./rectangle.js";
import {circleGeom} from "./circle.js";
import {arcGeom} from "./arc.js";
import {curveGeom} from "./curve.js";
import {areaGeom} from "./area.js";
import {pathGeom} from "./path.js";
//import {polylineGeom} from "./polyline.js";
//import {polygonGeom} from "./polygon.js";

//Geoms types
const geomsTypes = {
    //"polyline": polylineGeom,
    //"polygon": polygonGeom,
    "text": textGeom,
    "line": lineGeom,
    "rectangle": rectangleGeom,
    "circle": circleGeom,
    "arc": arcGeom,
    "segment": segmentGeom,
    "area": areaGeom,
    "path": pathGeom
};

//Get a geom object
export function getGeom (name) {
    return geomsTypes[name];
}

//build geom data
let buildGeomData = function (context, props) {
    //Get data in context
    let data = context.source(props.source);
    //Check for transform to apply
    if (isArray(props.transform) || isObject(props.transform)) {
        data = data.map(function (item) {
            return context.transform(item, props.transform);
        });
    }
    //Return the parsed data
    return data;
};

//Get geom data
let getGeomData = function (data, props) {
    if (props.type === "curve" || props.type === "area") {
        return data; //Return the entire dataset
    }
    //Return only the first item
    return data[0];
};

//Build geom element
let createGeomElement = function (geom, parent, index, group) {
    return parent.append(geom.tag).attr("id", `geom__${group}__${index}`);
};

//Get a geom element
let getGeomElement = function (geom, parent, index, group) {
    return parent.selectAll(`#geom__${group}__${index}`);
};

//Check if render props contain a geom props
let hasGeomRenderProps = function (props, geom) {
    let keys = Object.keys(props);
    for (let i = 0; i < keys.length; i++) {
        if (typeof geom.props[keys[i]] !== "undefined") {
            return true; //Found a geom prop
        }
    }
    //No geom props found
    return false;
};

//Get rendering props
let getGeomRenderProps = function (props, event) {
    let renderProps = {}; //props.render.mount;
    //Check if init props are provided
    if (typeof props.render["init"] === "object" && props.render["init"] !== null) {
        renderProps = Object.assign({}, props.render["init"]);
    }
    //Check the current event
    if (typeof props.render[event] === "object" && props.render[event] !== null) {
        renderProps = Object.assign({}, renderProps, props.render[event]);
    }
    //Return the final props
    return renderProps;
};

//Get geom groups
let getGeomGroups = function (props) {
    if (props.type === "group") {
        return (typeof props.geoms !== "undefined") ? objectValues(props.geoms) : [];
    }
    //Else: return this geom props as an array
    return [props];
};

//Apply geom style
let applyGeomStyle = function (context, datum, props, target) {
    let datumValue = (isArray(datum) === true) ? datum[0] : datum; //Get datum value
    return each(props, function (key, value) {
        if (value !== null && isStyleName(key) === true) {
            return setStyle(target, key, context.value(props[key], datumValue)); //Apply style
        }
    });
};

//Create a new geom node
export function createGeomNode (context, parent, props, key) {
    //Initialize the geom node
    let node = context.addNode({
        "id": `geom:${key}`,
        "type": "geom",
        "props": props,
        "targets": null,
        "parent": parent,
        "source": null,
        "value": [],
    });
    //Get the source data
    if (typeof props.source === "object" && props.source !== null) {
        if (typeof props.source.data === "string") {
            let source = context.data[props.source.data];
            Object.assign(node, {
                "source": source
            });
            //Add this geom node as a target node for this data object
            source.targets.add(node.id, node);
        }
    }
    //Get values sources
    getGeomGroups(node.props).forEach(function (groupProps) {
        return each(groupProps.render, function (key, renderProps) {
            return each(renderProps, function (propKey, propValue) {
                return getValueSources(context, propValue).forEach(function (source) {
                    //console.log(source);
                    source.targets.add(node.id, node); //Add this geom as a target node
                });
            })
        });
    });
    //Render this geom the first time
    //return updateShape(context, node, true);
}

//Update the provided node
export function updateGeomNode (context, node, forceRender) {
    //Check for force rendering the geom
    if (forceRender === true) {
        //Clean the parent node
        node.parent.empty();
        //Get data to apply
        Object.assign(node, {
            "value": buildShapeData(context, node.props)
        });
    }
    //Get geom groups
    getGeomGroups(node.props).forEach(function (props, groupIndex) {
        //Get the geom item
        let geom = getGeom(props.type);
        //Init props
        let updateProps = getGeomRenderProps(props, "update");
        let hoverProps = getGeomRenderProps(props, "hover");
        //Check for no force render
        if (forceRender === false) {
            //Check for no update render
            if (typeof props.render.update !== "object" || props.render.update === null) {
                return;
            }
            //Check if we should reload each rendered geom
            let reloadGeom = hasGeomRenderProps(props.render.update, geom);
            //Access to all items
            return getGeomData(node.value, props).forEach(function (data, index) {
                //Select the element attached to this data node
                let element = getGeomElement(geom, node.parent, index, groupIndex);
                //Check for render props
                if (reloadGeom === true) {
                    geom.render(context, data, updateProps, element); //Redraw this element
                }
                //Apply style props
                applyGeomStyle(context, data, props.render.update, element);
            });
        }
        //Build geoms for each data group
        getGeomData(node.value, props).forEach(function (data, index) {
            let element = createGeomElement(geom, node.parent, index, groupIndex);
            geom.render(context, data, updateProps, element);  // --> render init + update props
            //Apply style props
            applyGeomStyle(context, data, updateProps, element);
            //Register on hover events
            if (typeof props.render.hover === "object" && props.render.hover !== null) {
                //Register enter event listener --> apply hover props
                element.on("mouseenter", function (event) {
                    if (hasGeomRenderProps(props.render.hover, geom) === true) {
                        geom.render(context, data, hoverProps, element); //Redraw this element
                    }
                    //Apply style props
                    applyGeomStyle(context, data, props.render.hover, element);
                });
                //Check for update props provided
                if (typeof props.render.update === "object" && props.render.update !== null) {
                    //Register on leave event listener --> apply update props to this element
                    element.on("mouseleave", function (event) {
                        if (hasGeomRenderProps(props.render.update, geom) === true) {
                            geom.render(context, data, updateProps, element); //Redraw this element
                        }
                        //Apply style props
                        applyGeomStyle(context, data, props.render.update, element);
                    });
                }
            }
            //Register additional event listening
            each(props.on, function (eventIndex, eventProps) {
                return setEvent(element, eventProps.type, function (event) {
                    //Check for signal update event
                    if (isString(eventProps.state) && isString(eventProps.value)) {
                        //Register an action to update this state
                        let stateNode = context.state[eventProps.state];
                        if (typeof stateNode !== "undefined") {
                            context.addAction(stateNode, context.expression(eventProps.value, {
                                "datum": data,
                                "event": event //TODO: process event values
                            }));
                            //Trigger the update action
                            context.render();
                        }
                    }
                    //Check for fire this event to the api
                    if (isString(eventProps.fire)) {
                        return context.events.dispatchEvent(eventProps.fire, event, data);
                    }
                });
            });
            //Register tooltip handlers
            if (typeof props.tooltip === "object" && props.tooltip !== null) {
                setTooltipEvents(context, element, data, props.tooltip);
            }
        });
    });
}

<commit_msg>render: added panels to geoms manager and renderer<commit_after>import {isObject, isString, isArray} from "../../util.js";
import {each, values as objectValues} from "../../util.js";
import {propTypes, parseProps} from "../../props.js";
import {getValueSources} from "../../runtime/value.js";
import {getExpressionSources} from "../../runtime/expression.js";

import {setStyle, isStyleName} from "../style.js";
import {setTooltipEvents} from "../tooltip.js";
import {setEvent, isEventName} from "../events.js";

import {getPanelsElements} from "../panels.js";

import {textGeom} from "./text.js";
import {lineGeom} from "./line.js";
import {rectangleGeom} from "./rectangle.js";
import {circleGeom} from "./circle.js";
import {arcGeom} from "./arc.js";
import {curveGeom} from "./curve.js";
import {areaGeom} from "./area.js";
import {pathGeom} from "./path.js";
//import {polylineGeom} from "./polyline.js";
//import {polygonGeom} from "./polygon.js";

//Geoms types
const geomsTypes = {
    //"polyline": polylineGeom,
    //"polygon": polygonGeom,
    "text": textGeom,
    "line": lineGeom,
    "rectangle": rectangleGeom,
    "circle": circleGeom,
    "arc": arcGeom,
    "segment": segmentGeom,
    "area": areaGeom,
    "path": pathGeom
};

//Get a geom object
export function getGeom (name) {
    return geomsTypes[name];
}

//build geom data
let buildGeomData = function (context, props) {
    //Get data in context
    let data = context.source(props.source);
    //Check for transform to apply
    if (isArray(props.transform) || isObject(props.transform)) {
        data = data.map(function (item) {
            return context.transform(item, props.transform);
        });
    }
    //Return the parsed data
    return data;
};

//Get geom data
let getGeomData = function (data, props) {
    if (props.type === "curve" || props.type === "area") {
        return data; //Return the entire dataset
    }
    //Return only the first item
    return data[0];
};

//Build geom element
let createGeomElement = function (geom, parent, index, group) {
    return parent.append(geom.tag).attr("id", `geom__${group}__${index}`);
};

//Get a geom element
let getGeomElement = function (geom, parent, index, group) {
    return parent.selectAll(`#geom__${group}__${index}`);
};

//Check if render props contain a geom props
let hasGeomRenderProps = function (props, geom) {
    let keys = Object.keys(props);
    for (let i = 0; i < keys.length; i++) {
        if (typeof geom.props[keys[i]] !== "undefined") {
            return true; //Found a geom prop
        }
    }
    //No geom props found
    return false;
};

//Get rendering props
let getGeomRenderProps = function (props, event) {
    let renderProps = {}; //props.render.mount;
    //Check if init props are provided
    if (typeof props.render["init"] === "object" && props.render["init"] !== null) {
        renderProps = Object.assign({}, props.render["init"]);
    }
    //Check the current event
    if (typeof props.render[event] === "object" && props.render[event] !== null) {
        renderProps = Object.assign({}, renderProps, props.render[event]);
    }
    //Return the final props
    return renderProps;
};

//Get geom groups
let getGeomGroups = function (props) {
    if (props.type === "group") {
        if (isArray(props.geoms) === true) {
            return props.geoms; //Return the array of geoms
        }
        else if (isObject(props.geos) === true) {
            return objectValues(props.geoms); //Return only values
        }
        //Default --> not valid geoms
        return [];
    }
    //Else: return this geom props as an array
    return [props];
};

//Apply geom style
let applyGeomStyle = function (context, datum, props, target) {
    let datumValue = (isArray(datum) === true) ? datum[0] : datum; //Get datum value
    return each(props, function (key, value) {
        if (value !== null && isStyleName(key) === true) {
            return setStyle(target, key, context.value(props[key], datumValue)); //Apply style
        }
    });
};

//Create a new geom node
export function createGeomNode (context, key, props) {
    //Initialize the geom node
    let node = context.addNode({
        "id": `geom:${key}`,
        "type": "geom",
        "props": props,
        "targets": null,
        "source": null,
        "value": [],
    });
    //Get the source data
    context.panels.targets.add(node.id, node); //Bind to panels
    if (typeof props.source === "object" && props.source !== null) {
        if (typeof props.source.data === "string") {
            let source = context.data[props.source.data];
            Object.assign(node, {
                "source": source
            });
            //Add this geom node as a target node for this data object
            source.targets.add(node.id, node);
        }
    }
    //Get values sources
    getGeomGroups(node.props).forEach(function (groupProps) {
        return each(groupProps.render, function (key, renderProps) {
            return each(renderProps, function (propKey, propValue) {
                return getValueSources(context, propValue).forEach(function (source) {
                    //console.log(source);
                    source.targets.add(node.id, node); //Add this geom as a target node
                });
            })
        });
    });
    //Render this geom the first time
    //return updateShape(context, node, true);
}

//Update the provided node
export function updateGeomNode (context, node, forceRender) {
    getPanelsElements(context, node.props.panel).forEach(function (element) {
        //Check if there is a group for this geom group
        let target = element.selectAll(`g[data-geom='${node.id}']`);
        if (target.length === 0) {
            target = element.append("g").attr("data-geom", node.id); //Create a new geom group
        }
        //Render the geom
        return renderGeom(context, node, target, forceUpdate);
    });
}

//Render a geom
let renderGeom = function (context, node, parent, forceUpdate) {
    //Check for force rendering the geom
    if (forceRender === true) {
        //Clean the parent node
        //node.parent.empty();
        parent.empty();
        //Get data to apply
        Object.assign(node, {
            "value": buildShapeData(context, node.props)
        });
    }
    //Get geom groups
    getGeomGroups(node.props).forEach(function (props, groupIndex) {
        //Get the geom item
        let geom = getGeom(props.type);
        //Init props
        let updateProps = getGeomRenderProps(props, "update");
        let hoverProps = getGeomRenderProps(props, "hover");
        //Check for no force render
        if (forceRender === false) {
            //Check for no update render
            if (typeof props.render.update !== "object" || props.render.update === null) {
                return;
            }
            //Check if we should reload each rendered geom
            let reloadGeom = hasGeomRenderProps(props.render.update, geom);
            //Access to all items
            return getGeomData(node.value, props).forEach(function (data, index) {
                //Select the element attached to this data node
                let element = getGeomElement(geom, parent, index, groupIndex);
                //Check for render props
                if (reloadGeom === true) {
                    geom.render(context, data, updateProps, element); //Redraw this element
                }
                //Apply style props
                applyGeomStyle(context, data, props.render.update, element);
            });
        }
        //Build geoms for each data group
        getGeomData(node.value, props).forEach(function (data, index) {
            let element = createGeomElement(geom, parent, index, groupIndex);
            geom.render(context, data, updateProps, element);  // --> render init + update props
            //Apply style props
            applyGeomStyle(context, data, updateProps, element);
            //Register on hover events
            if (typeof props.render.hover === "object" && props.render.hover !== null) {
                //Register enter event listener --> apply hover props
                element.on("mouseenter", function (event) {
                    if (hasGeomRenderProps(props.render.hover, geom) === true) {
                        geom.render(context, data, hoverProps, element); //Redraw this element
                    }
                    //Apply style props
                    applyGeomStyle(context, data, props.render.hover, element);
                });
                //Check for update props provided
                if (typeof props.render.update === "object" && props.render.update !== null) {
                    //Register on leave event listener --> apply update props to this element
                    element.on("mouseleave", function (event) {
                        if (hasGeomRenderProps(props.render.update, geom) === true) {
                            geom.render(context, data, updateProps, element); //Redraw this element
                        }
                        //Apply style props
                        applyGeomStyle(context, data, props.render.update, element);
                    });
                }
            }
            //Register additional event listening
            each(props.on, function (eventIndex, eventProps) {
                return setEvent(element, eventProps.type, function (event) {
                    //Check for signal update event
                    if (isString(eventProps.state) && isString(eventProps.value)) {
                        //Register an action to update this state
                        let stateNode = context.state[eventProps.state];
                        if (typeof stateNode !== "undefined") {
                            context.addAction(stateNode, context.expression(eventProps.value, {
                                "datum": data,
                                "event": event //TODO: process event values
                            }));
                            //Trigger the update action
                            context.render();
                        }
                    }
                    //Check for fire this event to the api
                    if (isString(eventProps.fire)) {
                        return context.events.dispatchEvent(eventProps.fire, event, data);
                    }
                });
            });
            //Register tooltip handlers
            if (typeof props.tooltip === "object" && props.tooltip !== null) {
                setTooltipEvents(context, element, data, props.tooltip);
            }
        });
    });
}

<|endoftext|>