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.
557 lines (552 loc) • 26.7 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 { customElement, state } from "lit/decorators.js";
import { randomColor,
//csvToDataset,
randomNumber, randomString,
//readFileInput,
} from "../../functions";
import style from "./ScatterDatasetInput.style";
import SlButton from "@shoelace-style/shoelace/dist/components/button/button.component.js";
import SlIconButton from "@shoelace-style/shoelace/dist/components/icon-button/icon-button.component.js";
import SlInput from "@shoelace-style/shoelace/dist/components/input/input.component.js";
import SlColorPicker from "@shoelace-style/shoelace/dist/components/color-picker/color-picker.component.js";
import SlPopup from "@shoelace-style/shoelace/dist/components/popup/popup.component.js";
import SlDropdown from "@shoelace-style/shoelace/dist/components/dropdown/dropdown.js";
import SlMenuItem from "@shoelace-style/shoelace/dist/components/menu-item/menu-item.component.js";
import IconTrash from "bootstrap-icons/icons/trash.svg";
import IconPlusCircle from "bootstrap-icons/icons/plus-circle.svg";
import IconCaretdown from "bootstrap-icons/icons/caret-down.svg";
import "../FileUploadButton";
import { SharedTestStateMixin } from "../../state";
import { LitElementWw } from "@webwriter/lit";
import { SelectDataset } from "./SelectDataset";
import { updateColorForFilter } from "../../helper-components/Charts/Scatterplot/drawScatterplot";
//TODO: Add teacher options,
//TODO:, file upload, dispatch event, change style for plus button
//TODO: Add margin
//TODO: Add style for selected Element
//TODO: Fix color by changing lightgreen and yellow to a better color
//TODO: Fix color of trash to red when hovering
// TODO: Fix style for the popup of buttons SelectDatasets?
// Fix the button add dataset
// Add the possibility to change the type of the column
let ScatterDatasetInput = class ScatterDatasetInput extends SharedTestStateMixin(LitElementWw) {
#addDatasetPopupOpen_accessor_storage = false;
get addDatasetPopupOpen() { return this.#addDatasetPopupOpen_accessor_storage; }
set addDatasetPopupOpen(value) { this.#addDatasetPopupOpen_accessor_storage = value; }
#addColumnPopupOpenIndex_accessor_storage = -1;
get addColumnPopupOpenIndex() { return this.#addColumnPopupOpenIndex_accessor_storage; }
set addColumnPopupOpenIndex(value) { this.#addColumnPopupOpenIndex_accessor_storage = value; }
addNewDataset() {
this.sharedState.scatterDatasets.sets.push({
color: randomColor(),
name: randomString(),
ids: [1],
labels: [],
typeOfEachData: [],
dimensional_data: [{ data: [], selected: false }],
//TODO: Change "" to undefined?
filter: { label: "", type: "none", index_in_labels: -1 },
});
}
addNumericColumn(index_inset) {
this.sharedState.scatterDatasets.sets[index_inset].labels.push(randomString());
this.sharedState.scatterDatasets.sets[index_inset].typeOfEachData.push("number");
this.sharedState.scatterDatasets.sets[index_inset].dimensional_data.map((data) => {
data.data.push(randomNumber());
});
}
addStringColumn(index_inset) {
this.sharedState.scatterDatasets.sets[index_inset].labels.push(randomString());
this.sharedState.scatterDatasets.sets[index_inset].typeOfEachData.push("string");
this.sharedState.scatterDatasets.sets[index_inset].dimensional_data.map((data) => {
data.data.push(randomString());
});
}
addRow(index_inset) {
this.sharedState.scatterDatasets.sets[index_inset].ids.push(this.sharedState.scatterDatasets.sets[index_inset].ids.length + 1);
this.sharedState.scatterDatasets.sets =
this.sharedState.scatterDatasets.sets.map((dataset, i) => {
i === index_inset
? dataset.dimensional_data.push({
data: dataset.typeOfEachData.map((d) => d === "number" ? randomNumber() : randomString()),
selected: false,
})
: dataset;
return dataset;
});
if (this.sharedState.scatterDatasets.selected.dataset_indexes.length === 1 &&
this.sharedState.scatterDatasets.selected.dataset_indexes[0] ===
index_inset &&
this.sharedState.scatterDatasets.sets[index_inset].filter?.label &&
this.sharedState.scatterDatasets.sets[index_inset].filter
?.index_in_labels !== -1 &&
this.sharedState.scatterDatasets.sets[index_inset].filter?.type !== "none") {
this.sharedState.scatterDatasets.sets[index_inset].filter.type ===
"number"
? updateColorForFilter(this.sharedState.scatterDatasets.sets[index_inset].filter
.index_in_labels, "number", this.sharedState.scatterDatasets.sets[index_inset])
: updateColorForFilter(this.sharedState.scatterDatasets.sets[index_inset].filter
.index_in_labels, "string", this.sharedState.scatterDatasets.sets[index_inset]);
}
}
deleteRow(index_inset, index_indata_scatterplot) {
this.sharedState.scatterDatasets.sets =
this.sharedState.scatterDatasets.sets.map((dataset, i) => {
i === index_inset
? dataset.dimensional_data.splice(index_indata_scatterplot, 1) &&
dataset.ids.splice(index_indata_scatterplot, 1)
: dataset;
return dataset;
});
}
deleteTableDataset(index_inset) {
this.sharedState.scatterDatasets.sets.splice(index_inset, 1);
// shift the indexes of the selected datasets
this.sharedState.scatterDatasets.selected.dataset_indexes =
this.sharedState.scatterDatasets.selected.dataset_indexes
.map((index) => {
if (index === index_inset) {
return null;
}
else if (index > index_inset) {
return index - 1;
}
else {
return index;
}
})
.filter((i) => i !== null);
}
deleteColumn(index_inset, index_inlabels) {
this.sharedState.scatterDatasets.sets =
this.sharedState.scatterDatasets.sets.map((dataset, i) => {
if (i === index_inset) {
dataset.labels.splice(index_inlabels, 1);
dataset.typeOfEachData.splice(index_inlabels, 1);
dataset.dimensional_data.map((data) => {
data.data.splice(index_inlabels, 1);
});
if (dataset.filter.index_in_labels === index_inlabels) {
dataset.filter = { label: "", type: "none", index_in_labels: -1 };
// Delete the color filter if the column is deleted
dataset.dimensional_data.map((data) => {
data.color = undefined;
});
}
}
return dataset;
});
// In case the deleted column was selected as x or y axis
if (this.sharedState.scatterDatasets.selected.dataset_indexes.includes(index_inset)) {
const deletedLabel = this.sharedState.scatterDatasets.sets[index_inset].labels[index_inlabels];
if (this.sharedState.scatterDatasets.selected.axis.x === deletedLabel ||
this.sharedState.scatterDatasets.selected.axis.y === deletedLabel) {
// if there are multiple datasets, deselect current dataset
if (this.sharedState.scatterDatasets.selected.dataset_indexes.length > 1) {
this.sharedState.scatterDatasets.selected.dataset_indexes.forEach((index, i) => {
if (index === index_inset) {
this.sharedState.scatterDatasets.selected.dataset_indexes.splice(i, 1);
}
});
// if there is only one dataset, deselect the axis
}
else {
if (this.sharedState.scatterDatasets.selected.axis.x === deletedLabel) {
this.sharedState.scatterDatasets.selected.axis.x = undefined;
}
else {
this.sharedState.scatterDatasets.selected.axis.y = undefined;
}
}
}
}
}
changeColor(color, index_inset) {
this.sharedState.scatterDatasets.sets[index_inset].color = color;
}
render() {
return html `${this.sharedState.teacherOptions.addRemoveData
? html `<div class="btn-wrapper">
<sl-button
slot="anchor"
label="Add ${this.sharedState.scatterDatasets.sets.length === 0
? "first"
: "new"} Dataset"
=${() => {
this.addNewDataset();
}}
>Add
${this.sharedState.scatterDatasets.sets.length === 0
? "First"
: "New"}
Dataset</sl-button
>
<wwci-select-dataset></wwci-select-dataset>
</div>`
: html ``}
${this.sharedState.scatterDatasets.sets.map((each_dataset, index_inset) => html `
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>
<div
style="display: flex; flex-direction: row; align-items: center"
>
${!this.sharedState.printable
? html `
<sl-color-picker
?disabled=${!(this.sharedState.editable ||
this.sharedState.teacherOptions.changeData)}
value="${this.sharedState.scatterDatasets.sets[index_inset].color}"
-input=${(e) => this.changeColor(e.target.value, index_inset)}
></sl-color-picker>
<sl-icon-button
?disabled=${!(this.sharedState.editable ||
this.sharedState.teacherOptions.addRemoveData)}
src=${IconTrash}
label="Settings"
=${() => this.deleteTableDataset(index_inset)}
></sl-icon-button>
<div class="input-wrapper">
<sl-input
?disabled=${!(this.sharedState.editable ||
this.sharedState.teacherOptions.changeData)}
type="text"
class="input"
value="${this.sharedState.scatterDatasets.sets[index_inset].name}"
-input=${(e) => {
this.sharedState.scatterDatasets.sets[index_inset].name = e.target.value;
}}
></sl-input>
</div>
`
: html ``}
</div>
</th>
${this.sharedState.scatterDatasets.sets[index_inset].labels.map((label, index_inlabels) => html `<th
style="background: ${this.sharedState.scatterDatasets
.sets[index_inset].typeOfEachData[index_inlabels] ===
"number"
? "lightgreen"
: "yellow"}"
>
<div
style="display: flex; flex-direction: row; align-items: center"
>
<div
class="input-wrapper"
style="background: ${this.sharedState.scatterDatasets
.sets[index_inset].typeOfEachData[index_inlabels] === "number"
? "lightgreen"
: "yellow"}"
>
<sl-input
?disabled=${!(this.sharedState.editable ||
this.sharedState.teacherOptions.changeData)}
type="text"
class="${this.sharedState.scatterDatasets.sets[index_inset].typeOfEachData[index_inlabels] === "number"
? "input-lightgreen"
: "input-yellow"}"
value="${label}"
-input=${(e) => {
const newValue = e.target.value;
this.sharedState.scatterDatasets.sets[index_inset].labels[index_inlabels] = newValue;
}}
-blur=${() => {
const labels = this.sharedState.scatterDatasets.sets[index_inset].labels;
const label = labels[index_inlabels];
if (labels.some((l, i) => l === label && i !== index_inlabels)) {
this.sharedState.scatterDatasets.sets[index_inset].labels[index_inlabels] = `${label} (1)`;
}
}}
></sl-input>
</div>
<sl-dropdown>
<sl-icon-button
slot="trigger"
src=${IconCaretdown}
label="Toggle dropdown"
></sl-icon-button>
<div class="dropdown-content">
${!this.sharedState.printable
? html ` <sl-menu-item
=${(e) => {
if (e.button !== 0)
return;
e.preventDefault();
e.stopPropagation();
this.deleteColumn(index_inset, index_inlabels);
}}
?disabled=${!(this.sharedState.editable ||
this.sharedState.teacherOptions
.addRemoveData)}
>
<sl-icon-button
class="delete-column-button"
src=${IconTrash}
label="Settings"
></sl-icon-button
>Delete Column</sl-menu-item
>`
: html ``}
${!this.sharedState.printable
? html `<sl-menu-item
type="checkbox"
.checked=${this.sharedState.scatterDatasets
.sets[index_inset].typeOfEachData[index_inlabels] === "number"
? false
: true}
=${() => {
this.sharedState.scatterDatasets.sets[index_inset].typeOfEachData[index_inlabels] =
"string";
this.sharedState.scatterDatasets.sets[index_inset].dimensional_data.map((data) => {
data.data[index_inlabels] =
data.data[index_inlabels].toString();
});
if (this.sharedState.scatterDatasets
.selected.dataset_indexes.length ===
1 &&
this.sharedState.scatterDatasets
.selected.dataset_indexes[0] ===
index_inset &&
this.sharedState.scatterDatasets.sets[index_inset].filter?.label === label &&
this.sharedState.scatterDatasets.sets[index_inset].filter?.index_in_labels ===
index_inlabels) {
this.sharedState.scatterDatasets.sets[index_inset].filter.type = "string";
updateColorForFilter(this.sharedState.scatterDatasets.sets[index_inset].filter.index_in_labels, "string", this.sharedState.scatterDatasets.sets[index_inset]);
}
}}
>Convert column values to
categorical</sl-menu-item
>
<sl-menu-item
type="checkbox"
.checked=${this.sharedState.scatterDatasets
.sets[index_inset].typeOfEachData[index_inlabels] === "number"
? true
: false}
=${() => {
this.sharedState.scatterDatasets.sets[index_inset].typeOfEachData[index_inlabels] =
"number";
this.sharedState.scatterDatasets.sets[index_inset].dimensional_data.map((data) => {
data.data[index_inlabels] = Number(data.data[index_inlabels])
? Number(data.data[index_inlabels])
: 0;
if (this.sharedState.scatterDatasets
.selected.dataset_indexes.length ===
1 &&
this.sharedState.scatterDatasets
.selected.dataset_indexes[0] ===
index_inset &&
this.sharedState.scatterDatasets.sets[index_inset].filter?.label === label &&
this.sharedState.scatterDatasets.sets[index_inset].filter?.index_in_labels ===
index_inlabels) {
this.sharedState.scatterDatasets.sets[index_inset].filter.type = "number";
updateColorForFilter(this.sharedState.scatterDatasets
.sets[index_inset].filter
.index_in_labels, "number", this.sharedState.scatterDatasets
.sets[index_inset]);
}
});
}}
>Convert column values to
numeric</sl-menu-item
>`
: html ``}
</div>
</sl-dropdown>
</div>
</th>`)}
</tr>
</thead>
<tbody>
${each_dataset.dimensional_data.map((each_data_scatterplot, index_inscatter) => html `
<tr>
<td>
<div
style="display: flex; flex-direction: row; align-items: center"
>
${!this.sharedState.printable
? html ` <sl-icon-button
?disabled=${!(this.sharedState.editable ||
this.sharedState.teacherOptions.addRemoveData)}
src=${IconTrash}
label="Settings"
=${() => this.deleteRow(index_inset, index_inscatter)}
></sl-icon-button>`
: html ``}
<div class="input-wrapper">
<sl-input
?disabled=${!(this.sharedState.editable ||
this.sharedState.teacherOptions.changeData)}
id="input"
type="text"
class="input"
value="${each_dataset.ids[index_inscatter]}"
-input=${(e) => (each_dataset.ids[index_inscatter] =
e.target.value)}
></sl-input>
</div>
</div>
</td>
${each_data_scatterplot.data.map((data, index_indata) => html `
<td
style="background: ${this.sharedState.scatterDatasets
.sets[index_inset].typeOfEachData[index_indata] ===
"number"
? "lightgreen"
: "yellow"}"
>
<div
style="display: flex; flex-direction: row; align-items: center"
>
<div
class="input-wrapper"
style="background: ${this.sharedState
.scatterDatasets.sets[index_inset]
.typeOfEachData[index_indata] === "number"
? "lightgreen"
: "yellow"}"
>
<sl-input
?disabled=${!(this.sharedState.editable ||
this.sharedState.teacherOptions.changeData)}
id="input-x-${index_inset}-${index_inscatter}-${index_indata}"
type="text or number"
class="${this.sharedState.scatterDatasets.sets[index_inset].typeOfEachData[index_indata] === "number"
? "input-lightgreen"
: "input-yellow"}"
value="${data}"
-input=${(e) => {
each_dataset.typeOfEachData[index_indata] ===
"number"
? (this.sharedState.scatterDatasets.sets[index_inset].dimensional_data[index_inscatter].data[index_indata] = Number(e.target.value))
: (this.sharedState.scatterDatasets.sets[index_inset].dimensional_data[index_inscatter].data[index_indata] = String(e.target.value));
if (this.sharedState.scatterDatasets.selected
.dataset_indexes.length === 1 &&
this.sharedState.scatterDatasets.selected
.dataset_indexes[0] === index_inset &&
this.sharedState.scatterDatasets.sets[index_inset].filter?.label ===
each_dataset.labels[index_indata] &&
this.sharedState.scatterDatasets.sets[index_inset].filter?.index_in_labels === index_indata) {
this.sharedState.scatterDatasets.sets[index_inset].filter.type === "number"
? updateColorForFilter(index_indata, "number", this.sharedState.scatterDatasets.sets[index_inset])
: updateColorForFilter(index_indata, "string", this.sharedState.scatterDatasets.sets[index_inset]);
}
}}
></sl-input>
</div>
</div>
</td>
`)}
</tr>
`)}
</tbody>
</table>
${this.sharedState.editable ||
(this.sharedState.teacherOptions.addRemoveData &&
!this.sharedState.printable)
? html `
<sl-popup
placement="right"
strategy="fixed"
.active=${this.addColumnPopupOpenIndex === index_inset}
style="z-index: 10"
>
<sl-icon-button
slot="anchor"
src=${IconPlusCircle}
label="Add Column"
=${(e) => {
this.addColumnPopupOpenIndex = index_inset;
e.target.focus();
}}
=${() => (this.addColumnPopupOpenIndex = -1)}
></sl-icon-button>
<div class="popup-content">
<sl-button
=${(e) => {
if (e.button !== 0)
return;
this.addStringColumn(index_inset);
this.addColumnPopupOpenIndex = -1;
}}
>
Categorical values
</sl-button>
<sl-button
=${(e) => {
if (e.button !== 0)
return;
this.addNumericColumn(index_inset);
this.addColumnPopupOpenIndex = -1;
}}
>
Numeric values
</sl-button>
</div>
</sl-popup>
`
: html ``}
</div>
${this.sharedState.teacherOptions.addRemoveData
? html `<sl-icon-button
=${() => this.addRow(index_inset)}
src=${IconPlusCircle}
label="Add Dataset"
></sl-icon-button>`
: html ``}
</div>
</div>
`)}`;
}
dispatchUpdateEvent() {
this.dispatchEvent(new CustomEvent("wwci-change", {
detail: {
value: this.sharedState.scatterDatasets,
},
bubbles: false,
composed: true,
}));
}
dispatchTitleUpdateEvent(title) {
this.dispatchEvent(new CustomEvent("wwci-title-change", {
detail: {
value: title,
},
bubbles: false,
composed: true,
}));
}
static styles = style;
static get scopedElements() {
return {
"sl-button": SlButton,
"sl-icon-button": SlIconButton,
"sl-color-picker": SlColorPicker,
"sl-input": SlInput,
"sl-popup": SlPopup,
"wwci-select-dataset": SelectDataset,
"sl-dropdown": SlDropdown,
"sl-menu-item": SlMenuItem,
};
}
};
__decorate([
state()
], ScatterDatasetInput.prototype, "addDatasetPopupOpen", null);
__decorate([
state()
], ScatterDatasetInput.prototype, "addColumnPopupOpenIndex", null);
ScatterDatasetInput = __decorate([
customElement("wwci-scatterdataset-input")
], ScatterDatasetInput);
export { ScatterDatasetInput };