/** A generic checkbox control. */
class Checkbox {
/**
* Create a checkbox control.
* @param {Object} options - Configuration options.
* @author Lawrence Fyfe
*/
constructor(options) {
this.options = options;
this.parentElement;
this.initialized = false;
this.checkboxDiv;
this.isChecked;
this.onChange;
}
/**
* Initialize this control by attaching it to the specified parent element.
* @param {element} parentElement - The parent element.
*/
initialize(parentElement) {
this.parentElement = parentElement instanceof d3.selection ? parentElement : d3.select(parentElement);
this.initialized = true;
}
/**
* Draw this control.
*/
draw() {
if (this.initialized) {
this.erase();
this.checkboxDiv = this.parentElement.append("div")
.attr("class", "checkboxDiv")
.append("input")
.attr("type", "checkbox")
.attr("name", this.options?.name ?? "")
.attr("value", this.options?.value ?? "on")
.property("checked", () => {
if (this.isChecked !== undefined) {
return this.isChecked();
}
})
.on("change", (event) => {
if (this.onChange !== undefined) {
this.onChange(event);
}
});
}
}
/**
* Erase this control.
*/
erase() {
if (this.checkboxDiv !== undefined) {
this.checkboxDiv.remove();
}
}
}
class Selector {
constructor() {
this.selectorDiv;
this.optionArray = [];
this.currentOption;
this.onSelectOption;
}
addOption(option) {
this.optionArray.push(option);
}
initialize(parentElement) {
if (parentElement instanceof d3.selection) {
this.selectorDiv = parentElement.append("div")
.attr("class", "selectorDiv");
}
else {
this.selectorDiv = d3.select(parentElement).append("div")
.attr("class", "selectorDiv");
}
}
draw() {
this.selectorDiv.append("select")
.attr("id", "selector")
.style("font-size", "1rem")
.style("margin-right", "2ch")
.style("margin-left", "2ch")
.style("margin-bottom", "2ex")
.on("change", (event) => {
this.currentOption = event.target.value;
if (this.onSelectOption !== undefined) {
this.onSelectOption(this.currentOption);
}
})
.selectAll("option")
.data(this.optionArray)
.enter()
.append("option")
.attr("value", (d) => d)
.property("selected", (d) => {
if (d === this.currentOption) {
return true;
}
else {
return false
}
})
.text((d) => d);
}
erase() {
if (this.selectorDiv !== undefined) {
this.selectorDiv.remove();
}
}
}
/**
* An SVG image as a button control. If an alternate image is specified, clicking will switch between the image and the alternate image.
*/
class SVGButton {
/**
* Create an SVG button control.
* @param {string} file - The SVG file name String.
* @param {Object} options - Configuration options.
* @author Lawrence Fyfe
*/
constructor(file, options) {
this.file = file;
this.options = options ?? {};
this.alternateFile = options?.alternateFile;
this.alternateSVG;
this.alternated = false;
this.svg;
this.parentElement;
this.width;
this.height;
this.initialized = false;
this.onClick;
this.buttonDescriptionSpan;
if (("opacity" in this.options) === false) {
this.options["opacity"] = "1.0";
}
this.fileIsReady = d3.svg(this.file).then((svg) => {
this.svg = svg.documentElement;
});
if (this.alternateFile !== undefined) {
this.alternateFileIsReady = d3.svg(this.alternateFile).then((alternateSVG) => {
this.alternateSVG = alternateSVG.documentElement;
});
}
}
/**
* Set the image file and (optionally) the alternate image file.
* @param {string} file - A string representing the SVG file name and path.
* @param {string} alternateFile - A string representing the alternate SVG file name and path.
*/
setFile(file, alternateFile) {
this.file = file;
this.alternateFile = alternateFile;
this.fileIsReady = d3.svg(this.file).then((svg) => {
this.svg = svg.documentElement;
});
if (this.alternateFile !== undefined) {
this.alternateFileIsReady = d3.svg(this.alternateFile).then((alternateSVG) => {
this.alternateSVG = alternateSVG.documentElement;
});
}
}
/**
* Set the value for the specified option if it exists in this control.
* @param {string} name - The name of the option.
* @param {string} value - The value to set for the option.
*/
setOption(name, value) {
if (name in this.options) {
this.options[name] = value;
}
}
/**
* Set all options for this control. Note that this method completely replaces all existing options with the specified options.
* @param {Object} options - The options to replace existing options.
*/
setOptions(options) {
this.options = options;
}
/**
* Initialize this control with the specified parameters.
* @param {element} parentElement - The parent element.
* @param {number} width - The width to use for the SVG image.
* @param {number} height - The height to use for the SVG image.
*/
initialize(parentElement, width, height) {
this.parentElement = parentElement instanceof d3.selection ? parentElement : d3.select(parentElement);
this.width = width;
this.height = height;
this.buttonDiv = this.parentElement.append("div")
.attr("class", "buttonDiv")
.on("click", () => {
if (this.onClick !== undefined) {
this.onClick();
}
})
.on("mouseenter", (event) => {
if (this.options?.description !== undefined) {
this.buttonDescriptionSpan.style("visibility", "visible");
}
})
.on("mouseleave", (event) => {
if (this.options?.description !== undefined) {
this.buttonDescriptionSpan.style("visibility", "hidden");
}
});
if (this.options?.description !== undefined) {
this.buttonDescriptionSpan = this.buttonDiv.append("span")
.attr("class", "buttonDescriptionSpan")
.style("visibility", "hidden")
.style("white-space", "nowrap")
.style("text-align", "center")
.style("position", "absolute")
.style("font-family", "sans-serif")
.style("font-size", "small")
.style("top", "110%")
.style("left", () => {
if (this.options?.descriptionAlign === "left") {
return "0";
}
else {
return "auto";
}
})
.style("right", () => {
if (this.options?.descriptionAlign === "right") {
return "0";
}
else {
return "auto";
}
})
.style("z-index", "1")
.text(this.options.description);
}
this.initialized = true;
}
/**
* Alternate the SVG images for this control.
*/
alternate() {
if (this.alternateFile !== undefined) {
if (this.alternated) {
this.alternated = false;
if (this.options?.description !== undefined) {
this.buttonDescriptionSpan.text(this.options.description);
}
}
else {
this.alternated = true;
if (this.options?.alternateDescription !== undefined) {
this.buttonDescriptionSpan.text(this.options.alternateDescription);
}
}
this.draw();
}
}
/**
* Set the SVG image for this control to the default image.
*/
reset() {
if (this.alternateFile !== undefined) {
this.alternated = false;
this.draw();
}
}
/**
* Draw this control.
*/
draw() {
if (this.initialized) {
this.erase();
this.buttonDiv.style("opacity", this.options.opacity);
if (this.alternateFile !== undefined && this.alternated) {
this.alternateFileIsReady.then(() => {
this.buttonDiv.append(() => this.alternateSVG)
.attr("class", "svgButton")
.attr("x", "0")
.attr("y", "0")
.attr("width", this.width)
.attr("height", this.height);
});
}
else {
this.fileIsReady.then(() => {
this.buttonDiv.append(() => this.svg)
.attr("class", "svgButton")
.attr("x", "0")
.attr("y", "0")
.attr("width", this.width)
.attr("height", this.height);
});
}
}
}
/**
* Erase this control.
*/
erase() {
if (this.buttonDiv !== undefined) {
this.buttonDiv.select("svg").remove();
}
}
}
/** An SVG image button that displays a checklist of items when clicked. */
class SVGChecklistButton {
/**
* Create an SVG checklist button control.
* @param {string} file - The SVG file name String.
* @param {Object} options - Configuration options.
* @author Lawrence Fyfe
*/
constructor(file, options) {
this.file = file;
this.options = options;
this.svg;
this.parentElement;
this.width;
this.height;
this.initialized = false;
this.itemArray = [];
this.onClick;
this.checklistOn = false;
this.buttonDiv;
this.checkListDiv;
this.buttonDescriptionSpan;
if (("opacity" in this.options) === false) {
this.options["opacity"] = "1.0";
}
this.fileIsReady = d3.svg(this.file).then((svg) => {
this.svg = svg.documentElement;
});
}
/**
* Set the SVG image file.
* @param {string} file - A string representing the SVG file name and path.
*/
setFile(file) {
this.file = file;
this.fileIsReady = d3.svg(this.file).then((svg) => {
this.svg = svg.documentElement;
});
}
/**
* Set the value for the specified option if it exists in this control.
* @param {string} name - The name of the option.
* @param {string} value - The value to set for the option.
*/
setOption(name, value) {
if (name in this.options) {
this.options[name] = value;
}
}
/**
* Set all options for this control. Note that this method completely replaces all existing options with the specified options.
* @param {Object} options - The options to replace existing options.
*/
setOptions(options) {
this.options = options;
}
/**
* Add the specified checklist item to this control.
* @param {Object} item - The item to add.
*/
addItem(item) {
this.itemArray.push(item);
}
/**
* Add an array of checklist item to this control.
* @param {Object} items - The array of items to add.
*/
addItems(items) {
this.itemArray.push(...items);
}
/**
* Initialize this control with the specified parameters.
* @param {element} parentElement - The parent element.
* @param {number} width - The width to use for the SVG image.
* @param {number} height - The height to use for the SVG image.
*/
initialize(parentElement, width, height) {
this.parentElement = parentElement instanceof d3.selection ? parentElement : d3.select(parentElement);
this.width = width;
this.height = height;
this.buttonDiv = this.parentElement.append("div")
.attr("class", "buttonDiv")
.on("click", () => {
if (this.checklistOn) {
this.checklistOn = false;
this.buttonDiv.style("opacity", "0.5");
this.eraseChecklist();
}
else {
this.checklistOn = true;
this.buttonDiv.style("opacity", "1.0");
this.drawChecklist();
}
if (this.onClick !== undefined) {
this.onClick();
}
})
.on("mouseenter", (event) => {
if (this.options?.description !== undefined) {
this.buttonDescriptionSpan.style("visibility", "visible");
}
})
.on("mouseleave", (event) => {
if (this.options?.description !== undefined) {
this.buttonDescriptionSpan.style("visibility", "hidden");
}
});
if (this.options?.description !== undefined) {
this.buttonDescriptionSpan = this.buttonDiv.append("span")
.attr("class", "buttonDescriptionSpan")
.style("visibility", "hidden")
.style("white-space", "nowrap")
.style("text-align", "center")
.style("position", "absolute")
.style("font-family", "sans-serif")
.style("font-size", "small")
.style("top", "110%")
.style("left", () => {
if (this.options?.descriptionAlign === "left") {
return "0";
}
else {
return "auto";
}
})
.style("right", () => {
if (this.options?.descriptionAlign === "right") {
return "0";
}
else {
return "auto";
}
})
.style("z-index", "1")
.text(this.options.description);
}
this.initialized = true;
}
/**
* Draw the specified checklist item.
* @param {Object} item - The item to draw.
*/
drawChecklistItem(item) {
var name = item.name + item.getLinkedNames();
var checkListItemDiv = this.checkListDiv.append("div")
.attr("class", "checkListItemDiv");
var checkboxDiv = checkListItemDiv.append("input")
.attr("type", "checkbox")
.property("checked", () => {
if (item.on === true) {
item.draw();
return true;
}
else {
item.erase();
return false;
}
})
.on("change", (event) => {
item.on = event.target.checked;
if (event.target.checked) {
item.draw();
}
else {
item.erase();
}
this.drawChecklist();
});
var textDiv = checkListItemDiv.append("div")
.attr("class", "textDiv")
.text(name);
}
/**
* Draw the checklist.
*/
drawChecklist() {
this.eraseChecklist();
this.checkListDiv = this.buttonDiv.append("div")
.attr("class", "checkListDiv")
.style("z-index", "2")
.style("position", "absolute")
.style("background-color", "#ffffff")
.style("display", "flex")
.style("flex-direction", "column")
.style("font-family", "sans-serif")
.style("padding", "10%")
.style("border-style", "solid")
.style("border-color", "rgb(128, 128, 128)")
.style("border-width", "1px")
.style("border-radius", "8px")
.style("white-space", "nowrap")
.style("transform", "translate(1.2vh, 1.2vw)")
.on("click", (event) => event.stopPropagation());
if (this.itemArray.length > 1) {
var selectAllListItem = this.checkListDiv.append("div")
.attr("class", "selectAllListItem");
var selectAllCheckboxDiv = selectAllListItem.append("input")
.attr("type", "checkbox")
.property("checked", () => {
var checkedCount = 0;
for (let i = 0; i < this.itemArray.length; i++) {
if (this.itemArray[i].on === true) {
checkedCount++;
}
}
return checkedCount === this.itemArray.length;
})
.on("change", (event) => {
for (let i = 0; i < this.itemArray.length; i++) {
this.itemArray[i].on = event.target.checked;
}
this.drawChecklist();
});
var selectAllTextDiv = selectAllListItem.append("div")
.attr("class", "selectAllTextDiv")
.text("Select All");
}
for (let i = 0; i < this.itemArray.length; i++) {
this.drawChecklistItem(this.itemArray[i]);
}
}
/**
* Erase the checklist.
*/
eraseChecklist() {
if (this.checkListDiv !== undefined) {
this.checkListDiv.remove();
}
}
/**
* Draw this control.
*/
draw() {
if (this.initialized) {
this.erase();
this.buttonDiv.style("opacity", this.options.opacity);
this.fileIsReady.then(() => {
this.buttonDiv.append(() => this.svg)
.attr("class", "svgButton")
.attr("x", "0")
.attr("y", "0")
.attr("width", this.width)
.attr("height", this.height);
});
}
}
/**
* Erase this control.
*/
erase() {
if (this.buttonDiv !== undefined) {
this.buttonDiv.select("svg").remove();
}
}
}
/** An SVG image as a switch button. */
class SVGSwitchButton {
/**
* Create an SVG button control for switching.
* @param {string} file - The SVG file name String.
* @param {Object} options - Configuration options.
* @author Lawrence Fyfe
*/
constructor(file, options) {
this.file = file;
this.options = options ?? {};
this.on = options?.on ?? false;
this.svg;
this.parentElement;
this.width;
this.height;
this.initialized = false;
this.onClick;
this.buttonDescriptionSpan;
this.fileIsReady = d3.svg(this.file).then((svg) => {
this.svg = svg.documentElement;
});
}
/**
* Set the SVG image file.
* @param {string} file - A string representing the SVG file name and path.
*/
setFile(file) {
this.file = file;
this.fileIsReady = d3.svg(this.file).then((svg) => {
this.svg = svg.documentElement;
});
}
/**
* Initialize this control with the specified parameters.
* @param {element} parentElement - The parent element.
* @param {number} width - The width to use for the SVG image.
* @param {number} height - The height to use for the SVG image.
*/
initialize(parentElement, width, height) {
this.parentElement = parentElement instanceof d3.selection ? parentElement : d3.select(parentElement);
this.width = width;
this.height = height;
this.buttonDiv = this.parentElement.append("div")
.attr("class", "buttonDiv")
.style("opacity", () => {
if (this.on) {
return "1.0";
}
else {
return "0.5";
}
})
.on("click", () => {
if (this.on) {
this.on = false;
this.buttonDiv.style("opacity", "0.5");
}
else {
this.on = true;
this.buttonDiv.style("opacity", "1.0");
}
if (this.onClick !== undefined) {
this.onClick();
}
})
.on("mouseenter", (event) => {
if (this.options?.description !== undefined) {
this.buttonDescriptionSpan.style("visibility", "visible");
}
})
.on("mouseleave", (event) => {
if (this.options?.description !== undefined) {
this.buttonDescriptionSpan.style("visibility", "hidden");
}
});
if (this.options?.description !== undefined) {
this.buttonDescriptionSpan = this.buttonDiv.append("span")
.attr("class", "buttonDescriptionSpan")
.style("visibility", "hidden")
.style("white-space", "nowrap")
.style("text-align", "center")
.style("position", "absolute")
.style("font-family", "sans-serif")
.style("font-size", "small")
.style("top", "110%")
.style("left", () => {
if (this.options?.descriptionAlign === "left") {
return "0";
}
else {
return "auto";
}
})
.style("right", () => {
if (this.options?.descriptionAlign === "right") {
return "0";
}
else {
return "auto";
}
})
.style("z-index", "1")
.text(this.options.description);
}
this.initialized = true;
}
/**
* Draw this control.
*/
draw() {
if (this.initialized) {
this.erase();
this.fileIsReady.then(() => {
this.buttonDiv.append(() => this.svg)
.attr("class", "svgButton")
.attr("x", "0")
.attr("y", "0")
.attr("width", this.width)
.attr("height", this.height);
});
}
}
/**
* Erase this control.
*/
erase() {
if (this.buttonDiv !== undefined) {
this.buttonDiv.select("svg").remove();
}
}
}
/** A generic text button. */
class TextButton {
/**
* Create a text button control.
* @param {Object} options - Configuration options.
* @author Lawrence Fyfe
*/
constructor(options) {
this.options = options;
this.parentElement;
this.width = "3ch";
this.height = "3ch";
this.initialized = false;
this.onClick;
}
/**
* Set the value for the specified option if it exists in this control.
* @param {string} name - The name of the option.
* @param {string} value - The value to set for the option.
*/
setOption(name, value) {
if (name in this.options) {
this.options[name] = value;
}
}
/**
* Set all options for this control. Note that this method completely replaces all existing options with the specified options.
* @param {Object} options - The options to replace existing options.
*/
setOptions(options) {
this.options = options;
}
/**
* Initialize this control with the specified parameters.
* @param {element} parentElement - The parent element.
* @param {number} width - The width.
* @param {number} height - The height.
*/
initialize(parentElement, width, height) {
this.parentElement = parentElement instanceof d3.selection ? parentElement : d3.select(parentElement);
if (width !== undefined) {
if (isNaN(width)) {
this.width = width;
}
else {
this.width = Math.round(width).toString() + "px";
}
}
if (height !== undefined) {
if (isNaN(height)) {
this.height = height;
}
else {
this.height = Math.round(height).toString() + "px";
}
}
this.buttonDiv = this.parentElement.append("div")
.attr("class", "buttonDiv");
this.initialized = true;
}
/**
* Draw this control.
*/
draw() {
if (this.initialized) {
this.erase();
this.button = this.buttonDiv.append("button")
.attr("type", "button")
.attr("class", "buttonControl")
.style("font-size", this.options.fontSize ?? "x-large")
.style("width", this.width)
.style("height", this.height)
.style("position", "relative")
.style("background-color", this.options.backgroundColor ?? "#ffffff")
.style("border-style", this.options.borderStyle ?? "solid")
.style("border-color", this.options.borderColor ?? "rgb(128, 128, 128)")
.style("border-width", this.options.borderWidth ?? "1px")
.style("border-radius", this.options.borderRadius ?? "8px")
.style("padding", "0")
.style("margin-left", this.options.marginLeft ?? "0")
.style("margin-right", this.options.marginRight ?? "0")
.style("opacity", this.options.opacity ?? "0.5")
.on("click", () => {
if (this.onClick !== undefined) {
this.onClick();
}
})
.on("mouseenter", (event) => {
this.button.style("border-color", "rgb(64, 64, 64)");
if (this.options.showDescription && this.options.description !== undefined) {
this.buttonDescriptionSpan.style("visibility", "visible");
}
})
.on("mouseleave", (event) => {
this.button.style("border-color", "rgb(128, 128, 128)");
if (this.options.showDescription && this.options.description !== undefined) {
this.buttonDescriptionSpan.style("visibility", "hidden");
}
})
.text(this.options.text ?? "");
if (this.options.description !== undefined) {
this.buttonDescriptionSpan = this.button.append("span")
.attr("class", "buttonDescriptionSpan")
.style("visibility", "hidden")
.style("white-space", "nowrap")
.style("text-align", "center")
.style("position", "absolute")
.style("font-family", "sans-serif")
.style("font-size", "small")
.style("top", "110%")
.style("left", "0%")
.style("z-index", "1")
.text(this.options.description);
}
}
}
/**
* Erase this control.
*/
erase() {
if (this.buttonDiv !== undefined) {
this.buttonDiv.selectAll("*").remove();
}
}
}
/** A checkbox for controlling the display status of a visual. */
class VisualCheckbox {
/**
* Create a visual control checkbox.
* @param {Object} visual - The visual to be controlled.
* @param {Object} options - Configuration options.
* @author Lawrence Fyfe
*/
constructor(visual, options) {
this.visual = visual;
this.options = options;
this.parentElement;
this.initialized = false;
this.checkboxDiv;
}
/**
* Initialize this control by attaching it to the specified parent element.
* @param {element} parentElement - The parent element.
*/
initialize(parentElement) {
this.parentElement = parentElement instanceof d3.selection ? parentElement : d3.select(parentElement);
this.initialized = true;
}
/**
* Draw this control.
*/
draw() {
if (this.initialized) {
this.checkboxDiv = this.parentElement.append("div")
.attr("class", "checkboxDiv")
.append("input")
.attr("type", "checkbox")
.property("checked", () => {
if (this.visual.on) {
return true;
}
else {
return false;
}
})
.on("change", (event) => {
this.visual.on = event.target.checked;
if (event.target.checked) {
this.visual.draw();
}
else {
this.visual.erase();
}
});
}
}
/**
* Erase this control.
*/
erase() {
if (this.checkboxDiv !== undefined) {
this.checkboxDiv.selectAll("*").remove();
}
}
}