webwriter-chart
Version:
ww-chart is an interactive data visualization widget for the WebWriter tool that implements various charts and diagrams for static and exploratory data visualization.
1,046 lines (1,042 loc) • 117 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { html } from "lit";
import * as d3 from "d3";
import { customElement, property } from "lit/decorators.js";
import { SharedTestStateMixin } from "../../../state";
import { OptionDefinition, Definition } from "./definition";
import SlRange from "@shoelace-style/shoelace/dist/components/range/range.js";
import SlButton from "@shoelace-style/shoelace/dist/components/button/button.js";
import SlIcon from "@shoelace-style/shoelace/dist/components/icon/icon.js";
import SlPopup from "@shoelace-style/shoelace/dist/components/popup/popup.component.js";
import SlMenu from "@shoelace-style/shoelace/dist/components/menu/menu.js";
import SlMenuItem from "@shoelace-style/shoelace/dist/components/menu-item/menu-item.js";
import SlTooltip from "@shoelace-style/shoelace/dist/components/tooltip/tooltip.component.js";
import IconFileBarGraph from "bootstrap-icons/icons/file-bar-graph.svg";
import IconHandIndex from "bootstrap-icons/icons/hand-index.svg";
import IconEye from "bootstrap-icons/icons/eye.svg";
import IconPencil from "bootstrap-icons/icons/pencil.svg";
import IconFilter from "bootstrap-icons/icons/filter.svg";
import { LitElementWw } from "@webwriter/lit";
import { addBrushingListeners, addDragingPointListeners, addDrawLineListeners, addGridListeners, addPointListeners, addRegressionLine, calculateCorrelation, calculateForRotation, deleteLineListeners, deletePointListeners, drawScatterplot, findIndexForLine, selectPointByClickingListeners, updateLineAndPoints, updateColorForFilter, drawScatterplot_singleDataset, drawScatterplot_singleDataset2, } from "./drawScatterplot";
import style from "./scatterplot.style";
let ScatterplotChart = class ScatterplotChart extends SharedTestStateMixin(LitElementWw) {
static styles = style;
#definition_accessor_storage = Definition;
get definition() { return this.#definition_accessor_storage; }
set definition(value) { this.#definition_accessor_storage = value; }
#addNode_accessor_storage = false;
get addNode() { return this.#addNode_accessor_storage; }
set addNode(value) { this.#addNode_accessor_storage = value; }
#selectNode_accessor_storage = false;
get selectNode() { return this.#selectNode_accessor_storage; }
set selectNode(value) { this.#selectNode_accessor_storage = value; }
#selectByClicking_accessor_storage = false;
get selectByClicking() { return this.#selectByClicking_accessor_storage; }
set selectByClicking(value) { this.#selectByClicking_accessor_storage = value; }
#selectByBrushing_accessor_storage = false;
get selectByBrushing() { return this.#selectByBrushing_accessor_storage; }
set selectByBrushing(value) { this.#selectByBrushing_accessor_storage = value; }
#selectedIndextoAddNote_accessor_storage = 0;
get selectedIndextoAddNote() { return this.#selectedIndextoAddNote_accessor_storage; }
set selectedIndextoAddNote(value) { this.#selectedIndextoAddNote_accessor_storage = value; }
#deselectByBrushing_accessor_storage = false;
get deselectByBrushing() { return this.#deselectByBrushing_accessor_storage; }
set deselectByBrushing(value) { this.#deselectByBrushing_accessor_storage = value; }
#editShowing_accessor_storage = false;
get editShowing() { return this.#editShowing_accessor_storage; }
set editShowing(value) { this.#editShowing_accessor_storage = value; }
#hideSelected_accessor_storage = false;
get hideSelected() { return this.#hideSelected_accessor_storage; }
set hideSelected(value) { this.#hideSelected_accessor_storage = value; }
#hideUnselected_accessor_storage = false;
get hideUnselected() { return this.#hideUnselected_accessor_storage; }
set hideUnselected(value) { this.#hideUnselected_accessor_storage = value; }
#deleteSelected_accessor_storage = false;
get deleteSelected() { return this.#deleteSelected_accessor_storage; }
set deleteSelected(value) { this.#deleteSelected_accessor_storage = value; }
#deleteUnselected_accessor_storage = false;
get deleteUnselected() { return this.#deleteUnselected_accessor_storage; }
set deleteUnselected(value) { this.#deleteUnselected_accessor_storage = value; }
#showAll_accessor_storage = true;
get showAll() { return this.#showAll_accessor_storage; }
set showAll(value) { this.#showAll_accessor_storage = value; }
#drawLine_accessor_storage = false;
get drawLine() { return this.#drawLine_accessor_storage; }
set drawLine(value) { this.#drawLine_accessor_storage = value; }
#filterOn_accessor_storage = false;
get filterOn() { return this.#filterOn_accessor_storage; }
set filterOn(value) { this.#filterOn_accessor_storage = value; }
#hoverCursorChange_accessor_storage = true;
get hoverCursorChange() { return this.#hoverCursorChange_accessor_storage; }
set hoverCursorChange(value) { this.#hoverCursorChange_accessor_storage = value; }
#hoverCursorToMove_accessor_storage = false;
//TODO: Use this attribute to change the cursor to a hand with a pointing finger
get hoverCursorToMove() { return this.#hoverCursorToMove_accessor_storage; }
set hoverCursorToMove(value) { this.#hoverCursorToMove_accessor_storage = value; }
#hoverCursorToPointer_accessor_storage = false;
get hoverCursorToPointer() { return this.#hoverCursorToPointer_accessor_storage; }
set hoverCursorToPointer(value) { this.#hoverCursorToPointer_accessor_storage = value; }
constructor(optionsDefinition = OptionDefinition) {
super();
this.sharedState.chart.optionDefinition = optionsDefinition;
this.sharedState.chart.options = Object.entries(optionsDefinition).reduce((acc, [key, value]) => {
acc[key] = this.sharedState.chart.options[key] ?? value.default;
return acc;
}, {});
}
// TODO: Fix this problem when moving lines
/*protected willUpdate(_changedProperties: PropertyValues) {
// if no properties changes it probably was a force update -> called by state library -> this.sharedState changed
if (_changedProperties.size === 0) {
this.updateScatterplot();
}
}*/
render() {
const firstSelectedDataset = this.sharedState.scatterDatasets.sets[this.sharedState.scatterDatasets.selected.dataset_indexes[0]];
return html `<div class="scatterplot-root">
<div class="scatterplot-wrapper">
<div class="scatterplot-svg"></div>
${this.sharedState.scatterDatasets.selected.dataset_indexes.length >
1 &&
(!this.sharedState.scatterDatasets.selected.axis.x ||
!this.sharedState.scatterDatasets.selected.axis.y)
? html `<div class="no-axis">Please select x and y axis</div>`
: html ``}
${this.sharedState.scatterDatasets.selected.dataset_indexes.length ===
1 &&
!this.sharedState.scatterDatasets.selected.axis.x &&
!this.sharedState.scatterDatasets.selected.axis.y
? html `<div class="no-axis">Please select x or y axis</div>`
: html ``}
${this.sharedState.scatterDatasets.selected.dataset_indexes.length === 0
? html `<div class="no-axis">Please select one or more datasets</div>`
: html ``}
<div class="button-container">
<sl-popup
placement="bottom"
strategy="fixed"
.active="${this.addNode}"
distance="7"
>
<span slot="anchor">
<sl-tooltip
hoist
placement="top-center"
content=${this.sharedState.chart.options.animation
? "To use this option, you must first disable the animation"
: this.totalDataLength === 0
? "To use this option, you must first add at least one data point to the selected dataset table(s)"
: this.sharedState.scatterDatasets.selected.dataset_indexes
.length === 1 &&
(firstSelectedDataset.typeOfEachData[firstSelectedDataset.labels.indexOf(this.sharedState.scatterDatasets.selected.axis.x)] !== "number" ||
firstSelectedDataset.typeOfEachData[firstSelectedDataset.labels.indexOf(this.sharedState.scatterDatasets.selected.axis.y)] !== "number")
? "The x and y axis must be numbers to use this draw line option"
: "Add a new data point"}
>
<sl-button
variant="default"
size="small"
circle
id="add-point-button"
@click=${(e) => {
this.addNode = true;
e.target.focus();
}}
.disabled=${this.sharedState.chart.options.animation ||
this.totalDataLength === 0 ||
(this.sharedState.scatterDatasets.selected.dataset_indexes
.length === 1 &&
(firstSelectedDataset.typeOfEachData[firstSelectedDataset.labels.indexOf(this.sharedState.scatterDatasets.selected.axis.x)] !== "number" ||
firstSelectedDataset.typeOfEachData[firstSelectedDataset.labels.indexOf(this.sharedState.scatterDatasets.selected.axis.y)] !== "number"))}
@blur=${() => (this.addNode = false)}
>
<sl-icon src=${IconFileBarGraph} label="Data"></sl-icon>
</sl-button>
</sl-tooltip>
</span>
<div class="popup-content">
${this.sharedState.scatterDatasets.selected.dataset_indexes.map((dataset_index, i) => {
const dataset = this.sharedState.scatterDatasets.sets[dataset_index];
return html `<sl-button
@mousedown=${(e) => {
if (e.button !== 0)
return;
this.selectedIndextoAddNote = i;
this.hoverCursorChange = false;
this.updateScatterplot();
}}
>
Add data of ${dataset.name}
</sl-button>`;
})}
</div>
</sl-popup>
<sl-popup
placement="bottom"
strategy="fixed"
.active=${this.selectNode}
distance="7"
>
<span slot="anchor">
<sl-tooltip
hoist
placement="top-center"
content=${this.sharedState.chart.options.animation
? "To use this option, you must first disable the animation"
: this.totalDataLength === 0
? "To use this option, you must first add at least one data point to the selected dataset table(s)"
: "(De)select data points"}
>
<sl-button
variant="default"
size="small"
circle
id="select-point-button"
.disabled=${this.totalDataLength === 0 ||
this.sharedState.chart.options.animation}
@click=${(e) => {
this.selectNode = true;
e.target.focus();
}}
@blur=${() => (this.selectNode = false)}
>
<sl-icon src=${IconHandIndex} label="Selection"></sl-icon>
</sl-button>
</sl-tooltip>
</span>
<div class="popup-content">
<sl-button
id="select-by-clicking"
@mousedown=${(e) => {
if (e.button !== 0)
return;
this.selectByClicking = true;
this.selectByBrushing = false;
this.deselectByBrushing = false;
this.deleteSelected = false;
this.deleteUnselected = false;
this.hideSelected = false;
this.hideUnselected = false;
this.showAll = false;
this.hoverCursorChange = false;
this.updateScatterplot();
}}
>
(De)select By Clicking
</sl-button>
<sl-button
id="select-by-brushing"
@mousedown=${(e) => {
if (e.button !== 0)
return;
this.selectByBrushing = true;
this.selectByClicking = false;
this.deselectByBrushing = false;
this.deleteSelected = false;
this.deleteUnselected = false;
this.hideSelected = false;
this.hideUnselected = false;
this.showAll = false;
this.updateScatterplot();
}}
>
Select By Brushing
</sl-button>
<sl-button
id="deselect-by-brushing"
.disabled=${this.selectedDataLength === 0}
@mousedown=${(e) => {
if (e.button !== 0)
return;
this.deselectByBrushing = true;
this.selectByBrushing = false;
this.selectByClicking = false;
this.deleteSelected = false;
this.deleteUnselected = false;
this.hideSelected = false;
this.hideUnselected = false;
this.showAll = false;
this.updateScatterplot();
}}
>
Deselect By Brushing
</sl-button>
</div>
</sl-popup>
<sl-popup
placement="bottom"
strategy="fixed"
.active=${this.editShowing}
distance="7"
>
<span slot="anchor">
<sl-tooltip
hoist
placement=${this.sharedState.chart.options.animation ||
this.totalDataLength === 0
? "top-end"
: "top-center"}
content=${this.sharedState.chart.options.animation
? "To use this option, you must first disable the animation"
: this.totalDataLength === 0
? "To use this option, you must first add at least one data point to the selected dataset table(s)"
: "Show data points"}
>
<sl-button
variant="default"
size="small"
circle
id="edit-button"
.disabled=${this.totalDataLength === 0 ||
this.sharedState.chart.options.animation}
@click=${(e) => {
this.editShowing = true;
e.target.focus();
}}
@blur=${() => (this.editShowing = false)}
>
<sl-icon src=${IconEye} label="View"></sl-icon>
</sl-button>
</sl-tooltip>
</span>
<div class="popup-content">
<sl-menu-item
type="checkbox"
?checked=${this.hideSelected}
.disabled=${this.selectedDataLength === 0}
@mousedown=${(e) => {
if (e.button !== 0)
return;
this.hideSelected = !this.hideSelected;
this.hideSelected ? (this.showAll = false) : null;
!this.hideSelected && !this.hideUnselected
? (this.showAll = true)
: null;
this.updateScatterplot();
}}
>Hide Selected</sl-menu-item
>
<sl-menu-item
type="checkbox"
?checked=${this.hideUnselected}
.disabled=${this.unselectedDataLength === 0}
@mousedown=${(e) => {
if (e.button !== 0)
return;
this.hideUnselected = !this.hideUnselected;
this.hideUnselected ? (this.showAll = false) : null;
!this.hideSelected && !this.hideUnselected
? (this.showAll = true)
: null;
this.updateScatterplot();
}}
>Hide Unselected</sl-menu-item
>
<sl-menu-item
type="checkbox"
?checked=${this.showAll}
@mousedown=${(e) => {
if (e.button !== 0)
return;
this.showAll = !this.showAll;
if (this.showAll) {
this.hideSelected = false;
this.hideUnselected = false;
}
this.updateScatterplot();
}}
>Show All</sl-menu-item
>
<sl-menu-item
type="checkbox"
?checked=${this.deleteSelected}
.disabled=${this.selectedDataLength === 0}
@mousedown=${(e) => {
if (e.button !== 0)
return;
this.deleteSelected = !this.deleteSelected;
if (this.deleteSelected) {
this.sharedState.scatterDatasets.sets.forEach((dataset, index) => {
for (let i = dataset.dimensional_data.length - 1; i >= 0; i--) {
if (this.sharedState.scatterDatasets.selected.dataset_indexes.includes(index)) {
if (dataset.dimensional_data[i].selected) {
dataset.dimensional_data.splice(i, 1);
dataset.ids.splice(i, 1);
}
}
}
});
}
this.updateScatterplot();
}}
>Delete Selected</sl-menu-item
>
<sl-menu-item
type="checkbox"
?checked=${this.deleteUnselected}
.disabled=${this.unselectedDataLength === 0}
@mousedown=${(e) => {
if (e.button !== 0)
return;
this.deleteUnselected = !this.deleteUnselected;
if (this.deleteUnselected) {
this.sharedState.scatterDatasets.sets.forEach((dataset, index) => {
for (let i = dataset.dimensional_data.length - 1; i >= 0; i--) {
if (this.sharedState.scatterDatasets.selected.dataset_indexes.includes(index)) {
if (!dataset.dimensional_data[i].selected) {
dataset.dimensional_data.splice(i, 1);
dataset.ids.splice(i, 1);
}
}
}
});
}
this.updateScatterplot();
}}
>Delete Unselected</sl-menu-item
>
</div>
</sl-popup>
<span slot="anchor">
<sl-tooltip
hoist
placement=${this.sharedState.chart.options.animation ||
(this.sharedState.scatterDatasets.selected.dataset_indexes
.length === 1 &&
(firstSelectedDataset.typeOfEachData[firstSelectedDataset.labels.indexOf(this.sharedState.scatterDatasets.selected.axis.x)] !== "number" ||
firstSelectedDataset.typeOfEachData[firstSelectedDataset.labels.indexOf(this.sharedState.scatterDatasets.selected.axis.y)] !== "number")) ||
this.totalDataLength === 0
? "top-end"
: "top-center"}
content=${this.sharedState.chart.options.animation
? "To use this option, you must first disable the animation"
: this.totalDataLength === 0
? "To use this option, you must first add at least one data point to the selected dataset table(s)"
: this.sharedState.scatterDatasets.selected.dataset_indexes
.length === 1 &&
(firstSelectedDataset.typeOfEachData[firstSelectedDataset.labels.indexOf(this.sharedState.scatterDatasets.selected.axis.x)] !== "number" ||
firstSelectedDataset.typeOfEachData[firstSelectedDataset.labels.indexOf(this.sharedState.scatterDatasets.selected.axis.y)] !== "number")
? "The x and y axis must be numbers to use this draw line option"
: "Draw a line in the scatterplot"}
>
<sl-button
variant="default"
size="small"
circle
id="add-point-button"
@click=${(e) => {
if (e.button !== 0)
return;
this.drawLine = true;
this.updateScatterplot();
}}
.disabled=${this.sharedState.chart.options.animation ||
(this.sharedState.scatterDatasets.selected.dataset_indexes
.length === 1 &&
(firstSelectedDataset.typeOfEachData[firstSelectedDataset.labels.indexOf(this.sharedState.scatterDatasets.selected.axis.x)] !== "number" ||
firstSelectedDataset.typeOfEachData[firstSelectedDataset.labels.indexOf(this.sharedState.scatterDatasets.selected.axis.y)] !== "number")) ||
this.totalDataLength === 0}
>
<sl-icon src=${IconPencil} label="Draw"></sl-icon>
</sl-button>
</sl-tooltip>
</span>
<sl-popup
placement="bottom"
strategy="fixed"
.active=${this.filterOn}
distance="7"
>
<span slot="anchor">
<sl-tooltip
hoist
placement=${this.sharedState.chart.options.animation ||
this.sharedState.scatterDatasets.selected.dataset_indexes
.length !== 1 ||
this.totalDataLength === 0
? "top-end"
: "top-center"}
content=${this.sharedState.chart.options.animation
? "To use this option, you must first disable the animation"
: this.sharedState.scatterDatasets.selected.dataset_indexes
.length !== 1
? "To use this option, you must first select only one dataset"
: this.totalDataLength === 0
? "To use this option, you must first add at least one data point to the dataset table"
: "Filter data points"}
>
<sl-button
variant="default"
size="small"
circle
id="filter-button"
@click=${(e) => {
this.filterOn = true;
e.target.focus();
}}
.disabled=${this.sharedState.chart.options.animation ||
this.sharedState.scatterDatasets.selected.dataset_indexes
.length !== 1 ||
this.totalDataLength === 0}
@blur=${() => (this.filterOn = false)}
>
<sl-icon src=${IconFilter} label="Filter"></sl-icon>
</sl-button>
</sl-tooltip>
</span>
<div class="popup-content">
${(firstSelectedDataset?.labels ?? []).map((label, i) => html `
<sl-menu-item
type="checkbox"
@mousedown=${(e) => {
if (e.button !== 0 || !firstSelectedDataset)
return;
e.preventDefault();
e.stopPropagation();
const index = firstSelectedDataset.filter.label === label ? -1 : i;
if (index !== -1) {
firstSelectedDataset.filter.label = label;
firstSelectedDataset.filter.index_in_labels = index;
const type = firstSelectedDataset.typeOfEachData[index];
firstSelectedDataset.filter.type = type;
updateColorForFilter(i, type, firstSelectedDataset);
}
else {
firstSelectedDataset.filter.label = "";
firstSelectedDataset.filter.index_in_labels = -1;
firstSelectedDataset.filter.type = "none";
firstSelectedDataset.dimensional_data.forEach((d) => {
d.color = firstSelectedDataset.color;
});
}
this.updateScatterplot();
}}
.checked=${firstSelectedDataset.filter.label === label}
>
${label}
</sl-menu-item>
`)}
</div>
</sl-popup>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th class="th_regression">Regression</th>
<th class="th_correlation">R</th>
</tr>
</thead>
<tbody class="regression-formulas"></tbody>
</table>
</div>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th class="th_line">Line</th>
</tr>
</thead>
<tbody class="line-formulas"></tbody>
</table>
</div>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th class="th_color">Color</th>
<th class="th_range">
${firstSelectedDataset?.filter?.label &&
firstSelectedDataset.filter.type === "number"
? firstSelectedDataset.filter.label
: "Label"}
</th>
</tr>
</thead>
<tbody class="color-formulas"></tbody>
</table>
</div>
${this.sharedState.chart.options.animation
? html `<sl-range
id="animationRange"
.value=${this.definition.animationTime}
@sl-change=${(e) => {
this.definition.animationTime = e.target.value;
this.updatePlot();
}}
min="1000"
max="5000"
step="500"
label="Animation Time"
></sl-range>`
: html ``}
</div>
</div>
</div>`;
}
get totalDataLength() {
let length = 0;
this.sharedState.scatterDatasets.selected.dataset_indexes.forEach((dataset_index) => {
length +=
this.sharedState.scatterDatasets.sets[dataset_index].dimensional_data
.length;
});
return length;
}
// Get the length of the selected data points
get selectedDataLength() {
let length = 0;
this.sharedState.scatterDatasets.sets.forEach((dataset) => {
dataset.dimensional_data.forEach((point) => {
if (point.selected) {
length++;
}
});
});
return length;
}
// Get the length of the unselected data points
get unselectedDataLength() {
let length = 0;
this.sharedState.scatterDatasets.sets.forEach((dataset) => {
dataset.dimensional_data.forEach((point) => {
if (!point.selected) {
length++;
}
});
});
return length;
}
firstUpdated() {
this.addNode = false;
this.selectNode = false;
this.selectByBrushing = false;
this.selectByClicking = false;
this.deselectByBrushing = false;
this.selectedIndextoAddNote = 0;
this.hideSelected = false;
this.hideUnselected = false;
this.deleteSelected = false;
this.deleteUnselected = false;
this.showAll = true;
this.editShowing = false;
this.drawLine = false;
this.filterOn = false;
this.hoverCursorChange = true;
this.hoverCursorToMove = false;
this.hoverCursorToPointer = false;
this.createScatterPlot();
// TODO: remove event listener on unmount
document.addEventListener("keypress", (e) => {
switch (e.key) {
case "Backspace":
case "Delete":
}
});
}
//updatePlot for new animation time
//TODO: Add margin right for bigger numbers and margin top
// TODO: Add name for scatterplot, maybe use the text feld?
// TODO: Name for each dataset unter the scatterplot
// TODO: Table for statistics: r, regression folrmula, mean, etc.
// TODO: Fix buttons convert and download image
// TODO: Given x and calculate y (geogbra)
// TODO: Show all regression line when cut
// TODO: see friendly error message and user drag, delete, add points from automa (WR)
// TODO: Fix edit button
// TODO: dymanic and manuell scaling for x and y axis
// TODO: Feedback for choose the right point
// TODO: Fix the random color problem
updatePlot() {
const animationRange = this.shadowRoot.querySelector("#animationRange");
this.definition.animationTime = Number(animationRange.value);
// delete the old scatter plot
const root = this.shadowRoot.querySelector(".scatterplot-root");
d3.select(root).selectAll("svg").remove();
// delete the left over tooltip
d3.select(root).selectAll(".tooltip").remove();
this.createScatterPlot(); // Redraw the scatter plot with the new animation time
}
// i is index of the selected label to filter
//Update the scatterplot after dragging a point
updateScatterplot() {
if (!this.hideSelected && !this.hideUnselected) {
this.showAll = true;
}
const root = this.shadowRoot.querySelector(".scatterplot-root");
d3.select(root).selectAll("svg").remove();
d3.select(root).selectAll(".tooltip").remove();
d3.select(root).selectAll("path.line").remove();
this.createScatterPlot();
}
// !IMPORTANT: TODO: Fix the drag behavior out of scatterplot area ?
createScatterPlot() {
const selectedDatasets = this.sharedState.scatterDatasets.sets.filter((_, i) => this.sharedState.scatterDatasets.selected.dataset_indexes.includes(i));
const x_axis_type = this.sharedState.scatterDatasets.selected.axis.x === undefined
? "none"
: selectedDatasets[0].typeOfEachData[selectedDatasets[0].labels.indexOf(this.sharedState.scatterDatasets.selected.axis.x)];
const y_axis_type = this.sharedState.scatterDatasets.selected.axis.y === undefined
? "none"
: selectedDatasets[0].typeOfEachData[selectedDatasets[0].labels.indexOf(this.sharedState.scatterDatasets.selected.axis.y)];
const root = this.shadowRoot.querySelector(".scatterplot-svg");
if (x_axis_type === "number" && y_axis_type === "number") {
const axisLabels = {
x: this.sharedState.scatterDatasets.selected.axis.x,
y: this.sharedState.scatterDatasets.selected.axis.y,
};
const datasets = this.sharedState.scatterDatasets.selected.dataset_indexes.map((dataset_index) => {
const set = this.sharedState.scatterDatasets.sets[dataset_index];
const index_x = set.labels.indexOf(this.sharedState.scatterDatasets.selected.axis.x);
const index_y = set.labels.indexOf(this.sharedState.scatterDatasets.selected.axis.y);
return set.dimensional_data.map((dimensional_data, i) => {
const d = {
x: dimensional_data.data[index_x],
y: dimensional_data.data[index_y],
selected: dimensional_data.selected,
color: selectedDatasets.length === 1 && dimensional_data.color
? dimensional_data.color
: set.color,
id: set.ids[i],
scatter_titel: set.name,
};
// !IMPORTANT: Keep the reference to the original dataset to update data when updating points directly!
return d;
});
});
const lineDatasetsWithCenterPoint = this.sharedState.lineDatasets.sets.length > 0
? this.sharedState.lineDatasets.sets.map((dataset) => dataset.data.concat([
{
x: (dataset.data[0].x + dataset.data[1].x) / 2,
y: (dataset.data[0].y + dataset.data[1].y) / 2,
},
]))
: [];
const { svg, scales, scatter, dimensions, tooltip } = drawScatterplot(root, datasets, axisLabels, {
hoverTooltip: this.sharedState.chart.options.hoverTooltip,
showOutliers: this.sharedState.chart.options.showOutliers,
hoverCursorChange: this.hoverCursorChange,
animation: this.sharedState.chart.options.animation,
}, this.sharedState.lineDatasets);
let animationTime = Number(this.definition.animationTime);
// Handle recht click to remove point
if (this.deletingPointByClicking) {
datasets.forEach((_, index) => {
svg
.selectAll(`circle.dataset-${index}`)
.on("contextmenu", (event) => {
deletePointListeners(event, svg, datasets, this.sharedState.scatterDatasets, scales);
//Re-render
this.updateScatterplot();
});
});
}
// Handle adding a new point
if (this.addNode &&
this.selectedIndextoAddNote !== null &&
this.selectedIndextoAddNote !== undefined &&
this.selectedIndextoAddNote >= 0 &&
this.selectedIndextoAddNote <
this.sharedState.scatterDatasets.selected.dataset_indexes.length) {
// Change cursor to a circle with the color of the selected dataset
let color;
this.sharedState.scatterDatasets.sets.forEach((set, index) => {
if (this.sharedState.scatterDatasets.selected.dataset_indexes.indexOf(index) === this.selectedIndextoAddNote) {
color = set.color;
}
});
let circleCursorSVG = `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="${color}" class="bi bi-circle-fill" viewBox="0 0 16 16">
<circle cx="8" cy="8" r="7"/>
</svg>
`;
let circleCursorURL = `data:image/svg+xml;base64,${btoa(circleCursorSVG)}`;
// Set the cursor style to the circle with the specified color
svg.style("cursor", `url(${circleCursorURL}) 8 8, auto`); // Centered cursor
svg.on("click", (event) => {
addPointListeners(event, svg, scales, this.sharedState.scatterDatasets, this.selectedIndextoAddNote);
this.addNode = false;
this.updateScatterplot();
});
this.hoverCursorChange = true;
}
// Clicking for selecting points
if (this.selectNode &&
this.selectByClicking &&
!this.selectByBrushing &&
!this.deselectByBrushing) {
// Change cursor to a hand with a pointing finger
svg.style("cursor", "pointer");
datasets.forEach((_, index) => {
svg.selectAll(`circle.dataset-${index}`).on("click", (event) => {
selectPointByClickingListeners(event, svg, datasets, scales, this.sharedState.scatterDatasets);
// Re-render
this.selectByClicking = false;
this.updateScatterplot();
});
});
this.hoverCursorChange = true;
}
// Dragging points to change their position
//TODO: Fix the problem with yelow marks when dragging (maybe focus problem)
//TODO: yellow marks appear after (eg. selectwith brush). The setting disappears
if (!(this.selectNode || this.selectByClicking || this.addNode)) {
addDragingPointListeners(svg, scatter, scales, this.sharedState.scatterDatasets, datasets, () => {
this.updateScatterplot();
});
}
// Handle brushing to select points (Select all points if they have the same coordinates)
// Handle deselecting points by brushing (deselect all points if they have the same coordinates)
if (this.selectNode && !this.selectByClicking) {
if (this.selectByBrushing || this.deselectByBrushing) {
addBrushingListeners(svg, scatter, dimensions, scales, this.sharedState.scatterDatasets, () => {
this.selectByBrushing = false;
this.deselectByBrushing = false;
this.updateScatterplot();
}, this.selectByBrushing ? "select" : "deselect");
}
}
//Hide selected points
if (this.hideSelected) {
scatter.selectAll("circle").each(function (d) {
if (d.selected) {
d3.select(this).style("display", "none");
}
});
}
//Hide unselected points
if (this.hideUnselected) {
scatter.selectAll("circle").each(function (d) {
if (!d.selected) {
d3.select(this).style("display", "none");
}
});
}
// Show all points
if (this.showAll) {
scatter.selectAll("circle").each(function (_) {
d3.select(this).style("display", "block");
});
}
// Draw a line in scatter plot
if (this.drawLine) {
// change cursor to pencil
// Define the pencil SVG as a string
let pencilCursorSVG = `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-pencil" viewBox="0 0 16 16">
<path d="M12.146.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1 0 .708l-10 10a.5.5 0 0 1-.168.11l-5 2a.5.5 0 0 1-.65-.65l2-5a.5.5 0 0 1 .11-.168zM11.207 2.5 13.5 4.793 14.793 3.5 12.5 1.207zm1.586 3L10.5 3.207 4 9.707V10h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.293zm-9.761 5.175-.106.106-1.528 3.821 3.821-1.528.106-.106A.5.5 0 0 1 5 12.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.468-.325"/>
</svg>
`;
// Encode the SVG to a Base64 data URL
let pencilCursorURL = `data:image/svg+xml;base64,${btoa(pencilCursorSVG)}`;
svg.style("cursor", `url(${pencilCursorURL}) 0 16, auto`);
addDrawLineListeners(svg, scales, (line) => {
this.sharedState.lineDatasets.sets.push({ data: line });
this.updateScatterplot();
});
this.drawLine = false;
}
//TODO: Limit the line to the scatterplot area
// Handle dragging lines to new positions
// Define the drag behavior for the center point of the line to move the entire line up down
// Define the drag behavior for the center point
let index_center = -1;
const dragCenter = d3
.drag()
.on("start", (event) => {
d3.select(event.sourceEvent.target).classed("selected-rect", true);
})
.on("drag", (event, d) => {
// Find index i of the line
index_center = findIndexForLine(scales, d, this.sharedState.lineDatasets, index_center);
if (index_center === -1) {
return;
}
const indexForEndPointsLeft = d[0].x <= d[1].x ? 0 : 1;
const indexForEndPointsRight = d[0].x <= d[1].x ? 1 : 0;
const [newX, newY] = d3.pointer(event, svg.node());
const dy = newY - (d[0].y + d[1].y) / 2;
const dx = newX - (d[0].x + d[1].x) / 2;
d[0].y += dy;
d[1].y += dy;
d[0].x += dx;
d[1].x += dx;
// Update the visual position of the line and points
updateLineAndPoints(svg, scales, d, index_center, indexForEndPointsLeft, indexForEndPointsRight, false, this.sharedState.lineDatasets);
})
.on("end", () => {
this.updateScatterplot();
});
let index_right = -1;
// Define the drag behavior for the end points of the line to rotate the line
const dragEndPointsRight = d3
.drag()
.on("start", (event, _) => {
d3.select(event.sourceEvent.target).classed("selected-rect", true);
})
.on("drag", (event, d) => {
// Find index i of the line
index_right = findIndexForLine(scales, d, this.sharedState.lineDatasets, index_right);
if (index_right === -1) {
return;
}
// Rotate the line around the center point
const [newX, newY] = d3.pointer(event, svg.node());
// Calculate the new position of the line
d = calculateForRotation(d, newX, newY);
// Update the visual position of the line and points
updateLineAndPoints(svg, scales, d, index_right, 0, 1, false, this.sharedState.lineDatasets);
})
.on("end", () => {
this.updateScatterplot();
});
let index_left = -1;
const dragEndPointsLeft = d3
.drag()
.on("start", (event, _) => {
d3.select(event.sourceEvent.target).classed("selected-rect", true);
})
.on("drag", (event, d) => {
// Find index i of the line
index_left = findIndexForLine(scales, d, this.sharedState.lineDatasets, index_left);
if (index_left === -1) {
return;
}
// Rotate the line around the center point
const [newX, newY] = d3.pointer(event, svg.node());
// Calculate the new position of the line
d = calculateForRotation(d, newX, newY);
// Update the visual position of the line and points
updateLineAndPoints(svg, scales, d, index_left, 1, 0, true, this.sharedState.lineDatasets);
})
.on("end", () => {
this.updateScatterplot();
});
// TODO: Delete the line by right click
// Append the line and points
this.sharedState.lineDatasets.sets.forEach((dataset, i) => {
const x1 = scales.x(dataset.data[0].x);
const y1 = scales.y(dataset.data[0].y);
const x2 = scales.x(dataset.data[1].x);
const y2 = scales.y(dataset.data[1].y);
// Define the end points of the line
const endPointsLeft = { x: x1, y: y1 };
const endPointsRight = { x: x2, y: y2 };
// Append the line
svg
.append("line")
.attr("class", `line-${i}`)
.attr("x1", x1)
.attr("y1", y1)
.attr("x2", x2)
.attr("y2", y2)
.attr("stroke", "#69b3a2")
.attr("stroke-width", 2);
svg.select(`.line-${i}`).on("contextmenu", (_) => {
this.sharedState.lineDatasets.sets.splice(i, 1);
this.updateScatterplot();
});
// Define the center point of the line
const center = { x: (x1 + x2) / 2, y: (y1 + y2) / 2 };
// Append the center point
svg
.append("rect")
.datum([endPointsLeft, endPointsRight])
.attr("class", `center-point-${i}`)
.attr("x", center.x - 5)
.attr("y", center.y - 5)
.attr("width", 10) // Width of the rectangle
.attr("height", 10) // Height of the rectangle
.attr("fill", "white") // Fill color of the rectangle
.attr("stroke", "black")
.attr("stroke-width", 1.5)
.call(dragCenter);
svg
.select(`.center-point-${i}`)
.on("contextmenu", (event) => {
deleteLineListeners(event, svg, lineDatasetsWithCenterPoint, scales, this.sharedState.lineDatasets);
this.updateScatterplot();
})
.on("mouseover", () => {
svg.style("cursor", "move");
})
.on("mouseleave", () => {
svg.style("cursor", "default");
});
// Append the end point left
const end_point_left = svg
.append("rect")
.datum([endPointsLeft, endPointsRight])
.attr("class", `end-point-left-${i}`)
.attr("x", endPointsLeft.x - 5) // Position of the rectangle
.attr("y", endPointsLeft.y - 5) // 5 is half of the width and height of the rectangle
.attr("width", 10) // Width of the rectangle
.attr("height", 10) // Height of the rectangle
.attr("fill", "white") // Fill color of the rectangle
.attr("stroke", "black")
.attr("stroke-width", 1.5);
svg
.select(`.end-point-left-${i}`)
.on("contextmenu", (event) => {
deleteLineListeners(event, svg, lineDatasetsWithCenterPoint, scales, this.sharedState.lineDatasets);
this.updateScatterplot();
})
.on("mouseover", () => {
svg.style("cursor", "move");
})
.on("mouseleave", () => {
svg.style("cursor", "default");
});
// Append the end point right
const end_point_right = svg
.append("rect")
.datum([endPointsLeft, endPointsRight])
.attr("class", `end-point-right-${i}`)
.attr("x", endPointsRight.x - 5)
.attr("y", endPointsRight.y - 5)
.attr("width", 10) // Width of the rectangle
.attr("height", 10) // Height of the rectangle
.attr("fill", "white")
.attr("stroke", "black")
.attr("stroke-width", 1.5);
svg
.select(`.end-point-right-${i}`)
.on("contextmenu", (event) => {
deleteLineListeners(event, svg, lineDatasetsWithCenterPoint, scales, this.sharedState.lineDatasets);
this.updateScatterplot();
})
.on("mouseover", () => {
svg.style("cursor", "move");
})
.on("mouseleave", () => {
svg.style("cursor", "default");
});
end_point_left.call(dragEndPointsLeft);
end_point_right.call(dragEndPointsRight);
});
// For line formulas
const lineFormulaRoot = this.shadowRoot.querySelector(".line-formulas");
lineFormulaRoot.children
? [...lineFormulaRoot.children].forEach((child) => child.remove())
: null;
this.sharedState.lineDatasets.sets.forEach((dataset) => {
const tr = document.createElement("tr");
const td = document.createElement("td");
tr.appendChild(td);
const lineSpan = document.createElement("span");
td.appendChild(lineSpan);
lineFormulaRoot.appendChild(tr);
const lineFormula = `y = ${((dataset.data[1].y - dataset.data[0].y) /
(dataset.data[1].x - dataset.data[0].x)).toFixed(2)}x + ${(dataset.data[0].y -
((dataset.data[1].y - dataset.data[0].y) /
(dataset.data[1].x - dataset.data[0].x)) *
dataset.data[0].x).toFixed(2)}`;
lineSpan.innerText = lineFormula;
});
// For scatter plot with regression
const regressionFormulaRoot = this.shadowRoot.querySelector(".regression-formulas");
[...regressionFormulaRoot.children].forEach((child) => child.remove());
datasets.forEach((dataset) =>