d3-seating-chart
Version:
A simple but pleasant seating chart written using d3js
437 lines • 16.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const d3 = require("d3");
const style_inline_1 = require("./style.inline");
const showBehavior_enum_1 = require("./showBehavior.enum");
const selectionChangeEvent_model_1 = require("./selectionChangeEvent.model");
const D3SeatingChartDefaultConfig = {
showBehavior: showBehavior_enum_1.ShowBehavior.DirectDecendants,
allowManualSelection: true
};
class D3SeatingChart {
constructor(element) {
this.element = element;
this.margin = 20;
this.history = [];
this.zoomChangedListeners = [];
this.selectionChangeListeners = [];
this.selectedElements = [];
}
init(config) {
let svgSelection = d3.select(this.element);
let gSelection = svgSelection.select('g');
this.config = config;
this.uniqueIdentifier = `d3sc_${Math.round(Math.random() * 10000000000)}`;
this.element.setAttribute(this.uniqueIdentifier, '');
let style = document.createElement('style');
style.innerHTML = style_inline_1.InlineStyle.replace(/\{\}/g, this.uniqueIdentifier);
this.element.appendChild(style);
this.bindEvents();
this.zoom(gSelection, false);
}
stripStyles(selector) {
let svgSelection = d3.select(this.element);
svgSelection.selectAll(selector)
.attr('stroke', null)
.attr('stroke-width', null)
.attr('fill', null);
}
getBoard() {
return this.selectElement('[board]');
}
selectElement(query) {
return d3.select(this.element).select(query);
}
selectElements(query) {
return d3.select(this.element).selectAll(query);
}
goToBoard() {
this.zoom(this.getBoard());
}
clearHistory() {
this.history.length = 0;
}
canGoBack() {
return !!this.history.length;
}
goBack() {
this.history.pop();
if (this.history.length) {
this.zoom(this.history[this.history.length - 1]);
}
else {
this.goToBoard();
}
}
registerZoomChangeListener(fn) {
this.zoomChangedListeners.push(fn);
return () => {
let idx = this.zoomChangedListeners.indexOf(fn);
if (idx != -1) {
this.zoomChangedListeners.splice(idx, 1);
}
};
}
registerSelectionChangeListener(fn) {
this.selectionChangeListeners.push(fn);
return () => {
let idx = this.selectionChangeListeners.indexOf(fn);
if (idx != -1) {
this.selectionChangeListeners.splice(idx, 1);
}
};
}
zoom(selection, animate = true) {
let scaleTransform;
let translateTransform;
let boardSelection = this.getBoard();
let boundingBox = selection.node().getBBox();
if (selection.node() !== boardSelection.node()) {
if (selection != this.focusedElement) {
this.history.push(selection);
}
}
else {
this.clearHistory();
}
this.selectElements('.focused').classed('focused', false);
selection.classed('focused', true);
this.focusedElement = selection;
let all = boardSelection.selectAll(`*`);
let activeLayer = selection.selectAll('.focused > *');
let parentWidth = this.element.clientWidth;
let parentHeight = this.element.clientHeight;
let desiredWidth = parentWidth - this.margin * 2;
let desiredHeight = parentHeight - this.margin * 2;
let widthRatio = desiredWidth / boundingBox.width;
let heightRatio = desiredHeight / boundingBox.height;
let ratio = Math.min(widthRatio, heightRatio);
scaleTransform = `scale(${ratio})`;
let newX = (this.element.clientWidth / 2 - boundingBox.width * ratio / 2 - boundingBox.x * ratio);
let newY = (this.element.clientHeight / 2 - boundingBox.height * ratio / 2 - boundingBox.y * ratio);
translateTransform = `translate(${newX},${newY})`;
let currentTransform = selection.attr('transform');
if (!currentTransform) {
currentTransform = 'translate(0, 0)scale(1)';
}
if (this.config.showBehavior !== showBehavior_enum_1.ShowBehavior.All) {
let hideList = this.getHideList(selection);
let showList = this.getShowList(selection);
hideList
.style('opacity', 1)
.transition()
.duration(animate ? 300 : 0)
.style('opacity', 0);
showList.transition()
.style('opacity', 0)
.duration(animate ? 300 : 0)
.style('opacity', 1);
}
boardSelection.transition()
.duration(animate ? 300 : 0)
.attr('transform', `${translateTransform}${scaleTransform}`);
let tmpListeners = this.zoomChangedListeners.concat([]);
tmpListeners.forEach((listener) => {
listener();
});
}
getShowList(selection) {
if (this.config.showBehavior === showBehavior_enum_1.ShowBehavior.AllDecendants) {
return selection.selectAll('.focused *');
}
else {
return selection.selectAll('.focused > *');
}
}
getHideList(selection) {
let boardSelection = this.getBoard();
let all = boardSelection.selectAll(`*`);
let children;
if (this.config.showBehavior === showBehavior_enum_1.ShowBehavior.AllDecendants) {
children = selection.selectAll('.focused *');
}
else {
children = selection.selectAll('.focused > *');
}
return d3.selectAll(all.nodes().filter((a) => {
return a != boardSelection.node() && a != selection.node() && children.nodes().indexOf(a) == -1 && (a.style.opacity === '' || a.style.opacity === '1');
}));
}
refresh() {
this.zoom(this.focusedElement, false);
}
bindEvents() {
let self = this;
this.selectElements('[zoom-control]').on('click', (d) => {
let ele = d3.event.srcElement;
let expose = ele.getAttribute('zoom-control');
if (expose) {
this.zoom(this.selectElement(`[zoom-target="${expose}"]`));
}
});
if (this.config.allowManualSelection) {
this.selectElements('[seat]').on('click', function () {
let selectionsChanged = false;
let ele = this;
if (!ele.hasAttribute('locked')) {
selectionsChanged = true;
if (ele.hasAttribute('selected')) {
self.selectedElements.splice(self.selectedElements.findIndex(x => x === ele), 1);
ele.removeAttribute('selected');
}
else {
self.selectedElements.push(ele);
ele.setAttribute('selected', '');
}
}
if (selectionsChanged) {
self.emitSelectionChangeEvent(selectionChangeEvent_model_1.SelectionChangeEventReason.SelectionChanged);
}
});
}
}
lock(ele, c = '', emitEvents = true) {
let selectionChanges = false;
ele = this.resolveElements(ele);
ele.forEach((e) => {
if (!e.hasAttribute('locked') || e.getAttribute('locked') != c) {
e.setAttribute('locked', c);
if (e.hasAttribute('selected')) {
e.removeAttribute('selected');
selectionChanges = true;
}
}
});
if (emitEvents && selectionChanges) {
this.emitSelectionChangeEvent(selectionChangeEvent_model_1.SelectionChangeEventReason.LockOverride);
}
}
unlockAll(c = '') {
if (c) {
this.unlock(`[locked="${c}"]`);
}
else {
this.unlock('[locked]');
}
}
unlock(ele) {
ele = this.resolveElements(ele);
ele.forEach((e) => {
if (e.hasAttribute('locked')) {
e.removeAttribute('locked');
}
});
}
deselectAll(emitEvents = true) {
this.deselect('[selected]', emitEvents);
}
deselect(ele, emitEvents = true) {
let selectionChanges = false;
ele = this.resolveElements(ele);
ele.forEach((e) => {
if (e.hasAttribute('selected')) {
selectionChanges = true;
e.removeAttribute('selected');
}
});
if (emitEvents && selectionChanges) {
this.emitSelectionChangeEvent(selectionChangeEvent_model_1.SelectionChangeEventReason.SelectionChanged);
}
}
select(ele, emitEvents = true) {
let selectionChanges = false;
ele = this.resolveElements(ele);
ele.forEach((e) => {
if (!e.hasAttribute('locked')) {
if (!e.hasAttribute('selected')) {
selectionChanges = true;
e.setAttribute('selected', '');
}
}
else {
throw new Error('Unable to select element because its locked ' + e.outerHTML);
}
});
if (emitEvents && selectionChanges) {
this.emitSelectionChangeEvent(selectionChangeEvent_model_1.SelectionChangeEventReason.SelectionChanged);
}
}
getClosestSeats(seatingAreaName, numSeats, contiguous = true, scatterFallback = true) {
let stage = this.selectElement('[stage]');
let seatingArea = this.selectElement(`[seating-area="${seatingAreaName}"]`);
let seats = seatingArea.selectAll('[seat]').nodes();
let stageBBox = stage.node().getBBox();
let seatingAreaBBox = seatingArea.node().getBBox();
let stageCenterX = stageBBox.x + stageBBox.width / 2;
let stageCenterY = stageBBox.y + stageBBox.height / 2;
let seatingAreaCenterX = seatingAreaBBox.x + seatingAreaBBox.width / 2;
let seatingAreaCenterY = seatingAreaBBox.y + seatingAreaBBox.height / 2;
let slopeX = seatingAreaCenterX - stageCenterX;
let slopeY = seatingAreaCenterY - stageCenterY;
let direction;
if (Math.abs(slopeX) > Math.abs(slopeY)) {
direction = slopeX < 0 ? 4 : 2;
}
else {
direction = slopeY < 0 ? 1 : 3;
}
let sortedSeats = seats.sort((a, b) => {
let aX = Math.round(parseFloat(a.getAttribute('x')));
let aY = Math.round(parseFloat(a.getAttribute('y')));
let bX = Math.round(parseFloat(b.getAttribute('x')));
let bY = Math.round(parseFloat(b.getAttribute('y')));
switch (direction) {
case 1:
if (aY < bY) {
return 1;
}
else if (aY > bY) {
return -1;
}
else {
if (aX < bX) {
return 1;
}
else if (aX > bX) {
return -1;
}
else {
return 0;
}
}
case 2:
if (aX > bX) {
return 1;
}
else if (aX < bX) {
return -1;
}
else {
if (aY > bY) {
return 1;
}
else if (aY < bY) {
return -1;
}
else {
return 0;
}
}
case 3:
if (aY > bY) {
return 1;
}
else if (aY < bY) {
return -1;
}
else {
if (aX < bX) {
return 1;
}
else if (aX > bX) {
return -1;
}
else {
return 0;
}
}
case 4:
if (aX < bX) {
return 1;
}
else if (aX > bX) {
return -1;
}
else {
if (aY > bY) {
return 1;
}
else if (aY < bY) {
return -1;
}
else {
return 0;
}
}
}
});
if (contiguous) {
let sections = [];
let sortedSeatsCopy = sortedSeats.concat([]);
let j = 0;
do {
j++;
let br = -1;
let lastSeat;
for (let i = 0; i < sortedSeatsCopy.length; i++) {
let seat = sortedSeatsCopy[i];
if (seat.hasAttribute('locked')) {
br = i;
sortedSeatsCopy.splice(i, 1);
break;
}
else if (lastSeat) {
if (direction === 1 || direction === 3) {
let lsY = Math.round(parseFloat(lastSeat.getAttribute('y')));
let sY = Math.round(parseFloat(seat.getAttribute('y')));
if (lsY != sY) {
br = i;
break;
}
}
else {
let lsX = Math.round(parseFloat(lastSeat.getAttribute('x')));
let sX = Math.round(parseFloat(seat.getAttribute('x')));
if (lsX != sX) {
br = i;
break;
}
}
}
lastSeat = seat;
}
if (br == -1) {
sections.push(sortedSeatsCopy.splice(0, sortedSeatsCopy.length));
}
else {
sections.push(sortedSeatsCopy.splice(0, br));
}
} while (sortedSeatsCopy.length && j < 20);
for (let i = 0; i < sections.length; i++) {
let section = sections[i];
if (section.length >= numSeats) {
return section.splice(0, numSeats);
}
}
}
if (!contiguous || scatterFallback) {
return sortedSeats.filter(x => !x.hasAttribute('locked')).splice(0, numSeats);
}
return [];
}
emitSelectionChangeEvent(r) {
let tmpListeners = this.selectionChangeListeners.concat([]);
tmpListeners.forEach((listener) => {
listener({
reason: r,
selection: this.selectedElements.concat([])
});
});
}
resolveElements(ele) {
if (typeof (ele) === 'string') {
ele = this.selectElements(ele).nodes();
}
else if (!(ele instanceof Array)) {
ele = [ele];
}
return ele;
}
static attach(element, config = D3SeatingChartDefaultConfig) {
let d3s = new D3SeatingChart(element);
d3s.init(config);
return d3s;
}
}
exports.D3SeatingChart = D3SeatingChart;
//# sourceMappingURL=d3SeatingChart.js.map