UNPKG

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.

418 lines (415 loc) 17.3 kB
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, css } from "lit"; import * as d3 from "d3"; import { customElement } from "lit/decorators.js"; import { SharedTestStateMixin } from "../../../state"; import { LitElementWw } from "@webwriter/lit"; let ScatterplotAdvancedChart = class ScatterplotAdvancedChart extends SharedTestStateMixin(LitElementWw) { render() { return html ` <div class="scatterplot-root"> <div class="scatterplot-wrapper"> <div class="scatterplot-svg"></div> </div> </div> `; } firstUpdated() { this.createScatterPlot(); } //TODO: Add case for just one string axis createScatterPlot() { const datasets = []; const datasets_copy = []; const singeledatasets = []; const singeledatasets_copy = []; // set the dimensions and margins of the graph const margin = { top: 10, right: 30, bottom: 50, left: 70 }, width = 550 - margin.left - margin.right, height = 400 - margin.top - margin.bottom; // Get the 2 dimentiional data from the shared state const indexOfX = this.sharedState.scatterplotAdvanced.selectedX === "none" ? -1 : this.sharedState.scatterplotAdvanced.scatterplotdatasetsAdvanced.labels.indexOf(this.sharedState.scatterplotAdvanced.selectedX); const indexOfY = this.sharedState.scatterplotAdvanced.selectedY === "none" ? -1 : this.sharedState.scatterplotAdvanced.scatterplotdatasetsAdvanced.labels.indexOf(this.sharedState.scatterplotAdvanced.selectedY); this.sharedState.scatterplotAdvanced.scatterplotdatasetsAdvanced.sets.forEach((dataset) => { !(indexOfX === -1) && !(indexOfY === -1) ? datasets_copy.push({ x: dataset.data[indexOfX], y: dataset.data[indexOfY], }) : !(indexOfX === -1) ? singeledatasets_copy.push({ p: dataset.data[indexOfX] }) : !(indexOfY === -1) ? singeledatasets_copy.push({ p: dataset.data[indexOfY] }) : null; }); let xlabel = this.sharedState.scatterplotAdvanced.selectedX; let ylabel = this.sharedState.scatterplotAdvanced.selectedY; const root = this.shadowRoot.querySelector(".scatterplot-svg"); // append the svg object to the body of the page const svg = d3 .select(root) .append("svg") .attr("viewBox", `0 0 ${width + margin.left + margin.right} ${height + margin.top + margin.bottom}`) .attr("preserveAspectRatio", "xMidYMid meet") .attr("width", width + margin.left + margin.right) .attr("height", height + margin.top + margin.bottom) .append("g") .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); // Add Gridlines const addGrid = (axis, scale, dimension, transform, offset) => { const grid = svg .append("g") .attr("class", "grid") .attr("transform", transform) .call(axis(scale) .tickSize(-dimension) .tickFormat(() => "") .tickSizeOuter(0)); grid .selectAll(".tick line") .attr("transform", `translate(${offset})`) .attr("stroke", "lightgrey"); grid.selectAll(".domain").remove(); }; let data_for_x_axis_copy; !(xlabel === "none") && !(ylabel === "none") ? (data_for_x_axis_copy = datasets_copy) : (data_for_x_axis_copy = singeledatasets_copy.map((d) => ({ x: d.p, y: "", }))); //TODO: max for number values // Add X axis let x; if (!(xlabel === "none") && data_for_x_axis_copy.length > 0) { x = typeof data_for_x_axis_copy[0].x === "number" ? d3 .scaleLinear() .domain([ 0, d3.max(data_for_x_axis_copy, (d) => Number(d.x)), ]) .range([0, width]) : d3 .scaleBand() .domain(data_for_x_axis_copy.map((d) => d.x)) .range([0, width]) .align(0) .paddingOuter(0) .paddingInner(0); if (typeof data_for_x_axis_copy[0].x === "number") { svg .append("g") .attr("transform", "translate(0," + height + ")") .call(d3.axisBottom(x)); } else { let axis_x = svg .append("g") .attr("transform", "translate(0," + height + ")") .call(d3.axisBottom(x)); if (xlabel !== "none" && ylabel !== "none") { if (typeof data_for_x_axis_copy[0].x === "string" && typeof data_for_x_axis_copy[0].y === "number") { axis_x .selectAll(".tick text") .attr("transform", `translate(${-x.bandwidth() / 2}, 0)`); svg.selectAll(".tick line").remove(); } } } svg .append("text") .attr("class", "x label") .attr("text-anchor", "middle") .attr("x", width / 2) .attr("y", height + 40) .attr("style", "text-align: center;") .text(xlabel); // Add Gridlines if (!(typeof data_for_x_axis_copy[0].x === "number")) { addGrid(d3.axisBottom, x, height, `translate(0, ${height})`, `${-x.bandwidth() / 2}, 0`); } } let data_for_y_axis_copy; !(xlabel === "none") && !(ylabel === "none") ? (data_for_y_axis_copy = datasets_copy) : (data_for_y_axis_copy = singeledatasets_copy.map((d) => ({ x: "", y: d.p, }))); // Add Y axis let y; if (!(ylabel === "none") && data_for_y_axis_copy.length > 0) { y = typeof data_for_y_axis_copy[0].y === "number" ? d3 .scaleLinear() .domain([ 0, d3.max(data_for_y_axis_copy, (d) => Number(d.y)), ]) .range([height, 0]) : d3 .scaleBand() .domain(data_for_y_axis_copy.map((d) => d.y)) .range([height, 0]) .padding(0) .paddingOuter(0); if (typeof data_for_y_axis_copy[0].y === "number") { svg.append("g").call(d3.axisLeft(y)); } else { let axis_y = svg.append("g").call(d3.axisLeft(y)); if (typeof data_for_y_axis_copy[0].y === "string" && typeof data_for_y_axis_copy[0].x === "number") { axis_y .selectAll(".tick text") .attr("transform", `translate( 0, ${y.bandwidth() / 2})`); svg.selectAll(".tick line").remove(); } } svg .append("text") .attr("class", "y label") .attr("text-anchor", "middle") .attr("x", -((height + margin.bottom) / 2)) // to my x axis .attr("y", -40) // to my y axis .attr("transform", "rotate(-90)") .text(ylabel); // Add Gridlines if (!(typeof data_for_y_axis_copy[0].y === "number")) { addGrid(d3.axisLeft, y, width, null, `0, ${y.bandwidth() / 2}`); } } if (!(xlabel === "none") && !(ylabel === "none")) { if (typeof data_for_y_axis_copy[0].y === "string" && typeof data_for_y_axis_copy[0].x === "string") { addGrid(d3.axisLeft, y, width, null, `0, ${y.bandwidth() / 2}`); addGrid(d3.axisBottom, x, height, `translate(0, ${height})`, `${-x.bandwidth() / 2}, 0`); } } svg.selectAll(".domain, .tick line").attr("stroke", "lightgrey"); // Check if x or y is a string and if an offset needs to be applied this.sharedState.scatterplotAdvanced.scatterplotdatasetsAdvanced.sets.forEach((dataset) => { const color = this.sharedState.scatterplotAdvanced.scatterplotdatasetsAdvanced .color; let offset = 0; if (!(indexOfX === -1) && !(indexOfY === -1)) { const x = dataset.data[indexOfX]; const y = dataset.data[indexOfY]; // Check if x or y is a string and if an offset needs to be applied if (typeof x === "string" && typeof y === "number") { offset = calculateOffset(datasets, "x", x, y); } else if (typeof y === "string" && typeof x === "number") { offset = calculateOffset(datasets, "y", y, x); } else if (typeof y === "string" && typeof x === "string") { offset = calculateOffset(datasets, "y", y, x); } datasets.push({ x, y, offset, color }); } else if (indexOfX === -1 && !(indexOfY === -1)) { const y = dataset.data[indexOfY]; offset = calculateOffset(singeledatasets, "x_none", null, y); singeledatasets.push({ p: y, offset, color }); } else if (!(indexOfX === -1) && indexOfY === -1) { const x = dataset.data[indexOfX]; offset = calculateOffset(singeledatasets, "y_none", null, x); singeledatasets.push({ p: x, offset, color }); } }); // Calculate the offset for the scatterplot function calculateOffset(datasets, string_axis, this_value, other_value) { let offset = 0; const otherAxis = string_axis === "x" ? "y" : string_axis === "y" ? "x" : string_axis === "x_none" ? "y" : "x"; const filteredData = datasets.filter((d) => string_axis === "x" || string_axis === "y" ? d[string_axis] === this_value && d[otherAxis] === other_value : d["p"] === other_value); const count = filteredData.length; let distance = 10; string_axis === "x" ? (distance = calculateDistanceOfOffset(string_axis, this_value, other_value)) : string_axis === "y" ? (distance = calculateDistanceOfOffset(string_axis, other_value, this_value)) : string_axis === "x_none" ? (distance = calculateDistanceOfOffset(string_axis, this_value, other_value)) : (distance = calculateDistanceOfOffset(string_axis, other_value, this_value)); if (count > 0) { // Diameter of each point (10) offset = count * distance; } return offset; } function calculateDistanceOfOffset(string_axis, x_value, y_value) { //Diameter of each point (10) let defaultDistance = 10; // Default distance between points (center to center) const filteredData = string_axis === "x" || string_axis === "y" ? datasets_copy.filter((d) => d.x === x_value && d.y === y_value) : string_axis === "x_none" ? singeledatasets_copy.filter((d) => d.p === y_value) : singeledatasets_copy.filter((d) => d.p === x_value); const count = filteredData.length; // Number of data points const bandwidth = string_axis === "x" ? x.bandwidth() : string_axis === "y" ? y.bandwidth() : string_axis === "x_none" ? width : height; // Assuming x is a scale object with a bandwidth method let distance = defaultDistance; // Set initial distance to the default if (count * defaultDistance > bandwidth) { // If the total width of points exceeds the bandwidth, reduce the distance distance = defaultDistance - (count * defaultDistance - bandwidth + 10) / count; } return distance; } const scatter = svg; // if one of the axis is string and there are multiple value of the other axis, those values are the same, then the circles will be on top of each other if (!(xlabel === "none") && !(ylabel === "none")) { datasets.forEach((d) => { scatter .append("circle") .attr("cx", typeof d.x === "number" ? x(d.x) : typeof d.y === "number" ? x(d.x) + 5 + d.offset : x(d.x) + 5) // 5 is the radius of the circle .attr("cy", typeof d.y === "number" ? y(d.y) : y(d.y) - 5 - d.offset + y.bandwidth()) .attr("r", 5) .style("fill", d.color ?? "#417ca1") .style("opacity", 0.5); }); } if (xlabel === "none" && !(ylabel === "none")) { singeledatasets.forEach((d) => { scatter .append("circle") .attr("cx", 5 + d.offset) .attr("cy", typeof d.p === "number" ? y(d.p) : y(d.p) + y.bandwidth() / 2) .attr("r", 5) .style("fill", d.color ?? "#417ca1") .style("opacity", 0.5); }); } if (!(xlabel === "none") && ylabel === "none") { singeledatasets.forEach((d) => { scatter .append("circle") .attr("cx", typeof d.p === "number" ? x(d.p) : x(d.p) + x.bandwidth() / 2) .attr("cy", -5 + height - d.offset) .attr("r", 5) .style("fill", d.color ?? "#417ca1") .style("opacity", 0.5); }); } } static get scopedElements() { return {}; } }; ScatterplotAdvancedChart.styles = css ` :host { width: 100%; } .scatterplot-root { width: 100%; } .scatterplot-wrapper { display: flex; flex-direction: row; /* Align items in a row */ align-items: flex-start; align-items: flex-start; /* Align items to the top */ height: 100%; } .scatterplot-svg { flex-grow: 1; /* Allow the SVG to take available space */ } .button-container { margin-left: 10px; /* Optional: Add some space between the SVG and the button */ } table, th, tr, td { border: 0.5px solid black; border-collapse: collapse; } .th_regression { min-width: 10rem; } .th_correlation { min-width: 2.5rem; } .table-wrapper { margin-top: 20px; } table { display: inline-table; background: white; } .regression-formulas td { font-size: 12px; } svg { border: 1px solid black; flex: 1 0 0 width: 100%; } text { font-family: Helvetica; font-size: 12px; color: #333; } g { font-size: 12px; } .selected { opacity: 1 !important; stroke: black !important; stroke-width: 1px !important; } .selected-rect{ opacity: 1 !important; stroke: red !important; stroke-width: 1.5px !important; } .popup-content { display: flex; flex-direction: column; align-items: stretch; background: white; box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1); z-index: 10; } svg *, sl-icon { user-select: none; } `; ScatterplotAdvancedChart = __decorate([ customElement("scatterplot-advanced-chart") ], ScatterplotAdvancedChart); export { ScatterplotAdvancedChart };