diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c6c94ea..e9b4ea44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Dependencies of sample states between plots - Use attributes coming from Python - Point families implementation as PointSet +- Graph2D implementation ### Removed - Package Version checks is not implemented anymore diff --git a/cypress/snapshots/base/graph2d.cy.ts/GRAPH2D CANVAS -- should draw canvas-base.png b/cypress/snapshots/base/graph2d.cy.ts/GRAPH2D CANVAS -- should draw canvas-base.png index bb79d414..50f56eb5 100644 --- a/cypress/snapshots/base/graph2d.cy.ts/GRAPH2D CANVAS -- should draw canvas-base.png +++ b/cypress/snapshots/base/graph2d.cy.ts/GRAPH2D CANVAS -- should draw canvas-base.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:cf27d2cd3c1466831280429cb13a78ff3dfa1007f6eee8701d8b198e5e40094e -size 83482 +oid sha256:be1552352b38a1606a6573c952ca4c8de5b8b4ed536a5b0551a41421ca9f195a +size 57888 diff --git a/cypress/snapshots/base/histogram.cy.ts/HISTOGRAM CANVAS -- should draw canvas-base.png b/cypress/snapshots/base/histogram.cy.ts/HISTOGRAM CANVAS -- should draw canvas-base.png index 68d648e3..b76730a5 100644 --- a/cypress/snapshots/base/histogram.cy.ts/HISTOGRAM CANVAS -- should draw canvas-base.png +++ b/cypress/snapshots/base/histogram.cy.ts/HISTOGRAM CANVAS -- should draw canvas-base.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8cff320851838522c80d8032021d8191c2b6853ce93cba011e8db972dc728b78 -size 38567 +oid sha256:aab405a726a6c369f1cf9d600e5a43161c2f810a5638123fac2857288c7cc23f +size 38382 diff --git a/cypress/snapshots/base/histogram.cy.ts/HISTOGRAM CANVAS -- should scale and translate axes limits-base.png b/cypress/snapshots/base/histogram.cy.ts/HISTOGRAM CANVAS -- should scale and translate axes limits-base.png index 3cb4d06d..1614f9ad 100644 --- a/cypress/snapshots/base/histogram.cy.ts/HISTOGRAM CANVAS -- should scale and translate axes limits-base.png +++ b/cypress/snapshots/base/histogram.cy.ts/HISTOGRAM CANVAS -- should scale and translate axes limits-base.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:aa56bbf6ec8cbef76ca626755f2f5edd0de6330f26ae4e931e74b5b277100aa6 -size 40446 +oid sha256:011391508f32ece2f5973398065974e18e49c0804f7f8b00ff139058b8d9856c +size 40501 diff --git a/cypress/snapshots/base/histogram.cy.ts/HISTOGRAM CANVAS -- should select with rubber band-base.png b/cypress/snapshots/base/histogram.cy.ts/HISTOGRAM CANVAS -- should select with rubber band-base.png index 24ea4533..5738661f 100644 --- a/cypress/snapshots/base/histogram.cy.ts/HISTOGRAM CANVAS -- should select with rubber band-base.png +++ b/cypress/snapshots/base/histogram.cy.ts/HISTOGRAM CANVAS -- should select with rubber band-base.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:2c87e4b53e30d7f107d490ff8988de62ba3821d7ace8b21ad7b136e0cef81956 -size 38950 +oid sha256:f4593509b59f84f36f4c2e80f6e4a45b5b484e24555b0f0affdb6fd1857b90c5 +size 38802 diff --git a/cypress/snapshots/base/multiplot.cy.ts/MULTIPLOT CANVAS -- should add primitive group container in multiplot-base.png b/cypress/snapshots/base/multiplot.cy.ts/MULTIPLOT CANVAS -- should add primitive group container in multiplot-base.png index 8807be0b..0eb5bbf9 100644 --- a/cypress/snapshots/base/multiplot.cy.ts/MULTIPLOT CANVAS -- should add primitive group container in multiplot-base.png +++ b/cypress/snapshots/base/multiplot.cy.ts/MULTIPLOT CANVAS -- should add primitive group container in multiplot-base.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:41671ab0e221e36113287a4e49a45ee82e9b92dc9306f30c13c9d8d716e0145d -size 202006 +oid sha256:e4e6c1eb890483ce86ede658bff33ab37056658b74b7a453d9bd7c8e9c2a5994 +size 182876 diff --git a/cypress/snapshots/base/multiplot.cy.ts/MULTIPLOT CANVAS -- should draw canvas-base.png b/cypress/snapshots/base/multiplot.cy.ts/MULTIPLOT CANVAS -- should draw canvas-base.png index 8e3b0523..2a6dd1a0 100644 --- a/cypress/snapshots/base/multiplot.cy.ts/MULTIPLOT CANVAS -- should draw canvas-base.png +++ b/cypress/snapshots/base/multiplot.cy.ts/MULTIPLOT CANVAS -- should draw canvas-base.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:1a19cb3e3d2cc87e145fe8167259eb80406aa287af2c514f1c458962a6c6a25c -size 295028 +oid sha256:8862066f149b48238c4971d856544cd40499a53b0a4e1dcf198b53574c01ce93 +size 276052 diff --git a/cypress/snapshots/base/multiplot.cy.ts/MULTIPLOT CANVAS -- should remove primitive group from container in multiplot-base.png b/cypress/snapshots/base/multiplot.cy.ts/MULTIPLOT CANVAS -- should remove primitive group from container in multiplot-base.png index c3946ce6..3ac49a55 100644 --- a/cypress/snapshots/base/multiplot.cy.ts/MULTIPLOT CANVAS -- should remove primitive group from container in multiplot-base.png +++ b/cypress/snapshots/base/multiplot.cy.ts/MULTIPLOT CANVAS -- should remove primitive group from container in multiplot-base.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:adcc3eb2c2a1a02be6c626588ac4db756880781b4250ea7536681aed258b2697 -size 286229 +oid sha256:f7726922c2e389ee8568b1e09fbd5ca82a9425a9689d39ffa73ba077b7d19aa8 +size 267246 diff --git a/cypress/snapshots/base/multiplot.cy.ts/MULTIPLOT CANVAS -- should reorder all plots in canvas-base.png b/cypress/snapshots/base/multiplot.cy.ts/MULTIPLOT CANVAS -- should reorder all plots in canvas-base.png index 43c63abe..4e5fda5c 100644 --- a/cypress/snapshots/base/multiplot.cy.ts/MULTIPLOT CANVAS -- should reorder all plots in canvas-base.png +++ b/cypress/snapshots/base/multiplot.cy.ts/MULTIPLOT CANVAS -- should reorder all plots in canvas-base.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9f3ce0b5dcc35f26ff478827f036033645a425df32e4114f17492b5fd87add98 -size 289431 +oid sha256:dace020d94e6d20e2711637b430b1bca9972f3f5ffe811c50efdfa81b967f710 +size 273565 diff --git a/cypress/snapshots/base/plotscatter.cy.ts/PLOT SCATTER CANVAS -- should draw canvas-base.png b/cypress/snapshots/base/plotscatter.cy.ts/PLOT SCATTER CANVAS -- should draw canvas-base.png index 07763884..37f7c9d1 100644 --- a/cypress/snapshots/base/plotscatter.cy.ts/PLOT SCATTER CANVAS -- should draw canvas-base.png +++ b/cypress/snapshots/base/plotscatter.cy.ts/PLOT SCATTER CANVAS -- should draw canvas-base.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8fcc8aca2defdc47266912ef0aa752fe903840d28bd2d33b0a4127d9fc6d7c29 -size 215741 +oid sha256:f3b76f0ea87eebef7ca9527d4b831f91cf0df4fd5bbb81ed43c769252eb801ff +size 216273 diff --git a/cypress/snapshots/base/scattermatrix.cy.ts/PLOT SCATTER MATRIX CANVAS -- should draw canvas-base.png b/cypress/snapshots/base/scattermatrix.cy.ts/PLOT SCATTER MATRIX CANVAS -- should draw canvas-base.png index 830e9e07..e6d5ddd4 100644 --- a/cypress/snapshots/base/scattermatrix.cy.ts/PLOT SCATTER MATRIX CANVAS -- should draw canvas-base.png +++ b/cypress/snapshots/base/scattermatrix.cy.ts/PLOT SCATTER MATRIX CANVAS -- should draw canvas-base.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:f2d97cb85728695584cad5c68e7b578e4e7157b237befeed32d327b9d7ca014c -size 230695 +oid sha256:42def34d20bd5af3c6ac420de2efe8b2d327931c591dc3917e5343b557009448 +size 230645 diff --git a/cypress/templates/graph2d.template.html b/cypress/templates/graph2d.template.html index 76be0c76..a5a0419b 100644 --- a/cypress/templates/graph2d.template.html +++ b/cypress/templates/graph2d.template.html @@ -45,7 +45,7 @@ var data = $data; - var plot_data = new PlotData.PlotScatter(data, width, height, true, 0, 0, $canvas_id.id); + var plot_data = new PlotData.newGraph2D(data, width, height, true, 0, 0, $canvas_id.id); plot_data.define_canvas($canvas_id.id); plot_data.draw_initial(); plot_data.mouse_interaction(plot_data.isParallelPlot); diff --git a/plot_data/core.py b/plot_data/core.py index 442478c1..6ad8b58c 100644 --- a/plot_data/core.py +++ b/plot_data/core.py @@ -11,7 +11,7 @@ import tempfile import warnings import webbrowser -from typing import Dict, List, Tuple, Union +from typing import Dict, List, Tuple, Union # , Any import matplotlib.pyplot as plt from matplotlib.patches import Polygon, Circle, Arc @@ -30,6 +30,8 @@ import plot_data.colors from plot_data import templates +# CURVES_DATATYPE = Union(List[float], List[str], List[List[float]], List[Dict[str, Any]]) + def delete_none_from_dict(dict1): """ Delete input dictionary's keys where value is None. """ @@ -762,8 +764,7 @@ class Graph2D(Figure): :type x_variable: str :param y_variable: variable that you want to display on y axis :type y_variable: str - :param axis: an object containing all information needed for \ - drawing axis + :param axis: an object containing all information needed for drawing axis :type axis: Axis :param log_scale_x: True or False :type log_scale_x: bool @@ -801,6 +802,109 @@ def mpl_plot(self, ax=None, **kwargs): ax.set_xlabel(xname) ax.set_ylabel(yname) return ax +# TODO: commented code here is supposed to be used soon + # def graphs_to_curves(self): + # curves = [] + # for graph in self.graphs: + # x_coords = [] + # y_coords = [] + # for sample in graph.elements: + # x_coords.append(sample[self.attribute_names[0]]) + # y_coords.append(sample[self.attribute_names[1]]) + # line_width = graph.edge_style.line_width + # color = graph.edge_style.color_stroke + # dash_line = graph.edge_style.dashline + # marker = graph.point_style.shape + # name = graph.name + # curves.append(Curve(x_coords, y_coords, name, line_width=line_width, color=color, dash_line=dash_line, + # marker=marker)) + # return curves + + # def to_plot(self): + # return + + +# class Curve(PlotDataObject): + +# _KWARGS = ['line_width', 'color', 'dash_line', 'marker'] + +# def __init__(self, x_coords: Union(List[str], List[float]), y_coords: Union(List[str], List[float]) = None, +# name: str = '', **kwargs): +# self.x_coords, self.y_coords = self.buildCoords(x_coords, y_coords) +# self.line_width = None +# self.color = None +# self.dash_line = None +# self.marker = None +# self.setStyle(kwargs) + +# @staticmethod +# def buildCoords(x_coords: Union(List[str], List[float]), y_coords: Union(List[str], List[float])): +# if y_coords is None: +# return list(range(len(y_coords))), x_coords +# if len(x_coords) == len(y_coords): +# return x_coords, y_coords +# raise ValueError("x_coords and y_coords must be the same length.") + +# def setStyle(self, kwargs: Dict[str, Any]): +# for attribute in self._KWARGS: +# if attribute in kwargs: +# setattr(self, attribute, kwargs[attribute]) + +# @classmethod +# def fromPlot(cls, x_values: CURVES_DATATYPE, y_values: CURVES_DATATYPE, x_variable: str, y_variable: str, +# legend: List[str], **kwargs): +# if isinstance(x_values[0], float): +# if len(legend) != 1 and legend is not None: +# raise ValueError("x_values and legend must be the same length.") +# if legend is None: +# return cls(x_values, y_values, **kwargs) +# if len(legend) == 1: +# return cls(x_values, y_values, legend[0], **kwargs) + +# if isinstance(x_values[0], dict): +# if x_variable not in x_values[0]: +# raise ValueError(f"{x_variable} not in keys of x_values.") + +# x_coords = []; +# y_coords = []; +# for elements in x_values: +# x_coords.append(elements[x_variable]) +# y_coords.append(elements[y_variable]) +# return cls(x_coords, y_coords, legend[0], **kwargs) + +# raise TypeError("x_values must be a list of float or dict.") + + +# class Plot(Figure): + +# _plot_commands = "GRAPH_COMMANDS" + +# def __init__(self, x_values: CURVES_DATATYPE, y_values: CURVES_DATATYPE = None, x_variable: str = None, +# y_variable: str = None, axis: Axis = None, legend: List[str] = None, width: int = 750, +# height: int = 400, name: str = '', **kwargs): +# self.curves = self.buildCurves(x_values, y_values, x_variable, y_variable, legend, **kwargs) +# if axis is None: +# self.axis = Axis() +# else: +# self.axis = axis +# super().__init__(width=width, height=height, type_='graph2d', name=name) + +# @staticmethod +# def buildCurves(x_values: CURVES_DATATYPE, y_values: CURVES_DATATYPE, x_variable: str, y_variable: str, +# legend: List[str], **kwargs): +# if isinstance(x_values[0], (str, float, dict)): +# return [Curve.fromPlot(x_values=x_values, y_values=y_values, x_variable=x_variable, y_variable=y_variable, +# legend=legend, **kwargs)] + +# if isinstance(x_values[0], list): +# curves = [] +# for x_subvalues, y_subvalues, sub_legend in zip(x_values, y_values, legend): +# curves.append(Curve.fromPlot(x_values=x_subvalues, y_values=y_subvalues, x_variable=x_variable, +# y_variable=y_variable, legend=sub_legend, **kwargs)) +# return curves + +# if isinstance(x_values[0], Curve): +# return x_values class Heatmap(DessiaObject): @@ -844,10 +948,10 @@ class Scatter(Figure): _plot_commands = "SCATTER_COMMANDS" - def __init__(self, x_variable: str, y_variable: str, tooltip: Tooltip = None, point_style: PointStyle = None, - elements: List[Sample] = None, points_sets: List[PointFamily] = None, axis: Axis = None, - log_scale_x: bool = None, log_scale_y: bool = None, heatmap: Heatmap = None, heatmap_view: bool = None, - width: int = 750, height: int = 400, name: str = ''): + def __init__(self, x_variable: str = None, y_variable: str = None, tooltip: Tooltip = None, + point_style: PointStyle = None, elements: List[Sample] = None, points_sets: List[PointFamily] = None, + axis: Axis = None, log_scale_x: bool = None, log_scale_y: bool = None, heatmap: Heatmap = None, + heatmap_view: bool = None, width: int = 750, height: int = 400, name: str = ''): self.tooltip = tooltip self.attribute_names = [x_variable, y_variable] self.point_style = point_style diff --git a/plot_data/templates.py b/plot_data/templates.py index 20246116..c0709fc0 100644 --- a/plot_data/templates.py +++ b/plot_data/templates.py @@ -39,7 +39,8 @@    Cluster:     - +    +
@@ -70,7 +71,7 @@ plot_data.mouse_interaction(plot_data.isParallelPlot);""" GRAPH_COMMANDS = """ - var plot_data = new PlotData.PlotScatter(data, width, height, true, 0, 0, $canvas_id.id); + var plot_data = new PlotData.newGraph2D(data, width, height, true, 0, 0, $canvas_id.id); plot_data.define_canvas($canvas_id.id); plot_data.draw_initial(); plot_data.mouse_interaction(plot_data.isParallelPlot);""" diff --git a/script/plot_scatter.py b/script/plot_scatter.py index b4d4835d..0ce7f445 100644 --- a/script/plot_scatter.py +++ b/script/plot_scatter.py @@ -48,7 +48,7 @@ color_stroke=VIOLET, stroke_width=2, size=8, # 1, 2, 3 or 4 - shape='^') # 'circle', 'square' or 'crux' + shape='cross') # 'circle', 'square' or 'crux' # Finally, axis can be personalized too graduation_style = plot_data.TextStyle(text_color=BLUE, font_size=10, diff --git a/script/test_objects/graph_test.py b/script/test_objects/graph_test.py index 8c18596e..9f13743e 100644 --- a/script/test_objects/graph_test.py +++ b/script/test_objects/graph_test.py @@ -3,7 +3,7 @@ import math import plot_data -from plot_data.colors import BLACK, BLUE, RED +from plot_data.colors import BLUE, RED k = 0 @@ -17,7 +17,7 @@ # The previous line instantiates a dataset with limited arguments but # several customizations are available -point_style = plot_data.PointStyle(color_fill=RED, color_stroke=BLACK, shape='crux') +point_style = plot_data.PointStyle(color_fill=RED, color_stroke=RED, shape='crux') edge_style = plot_data.EdgeStyle(color_stroke=BLUE, dashline=[10, 5]) custom_dataset = plot_data.Dataset(elements=elements1, name='I = f(t)', @@ -33,4 +33,4 @@ elements2.append({'time': T2[k], 'electric current': I2[k]}) dataset2 = plot_data.Dataset(elements=elements2, name='I2 = f(t)') -graph2d = plot_data.Graph2D(graphs=[dataset1, dataset2], x_variable='time', y_variable='electric current') +graph2d = plot_data.Graph2D(graphs=[custom_dataset, dataset2], x_variable='time', y_variable='electric current') diff --git a/src/multiplots.ts b/src/multiplots.ts index b9f9aad6..6b216935 100644 --- a/src/multiplots.ts +++ b/src/multiplots.ts @@ -1,7 +1,7 @@ import {PlotData, Interactions} from './plot-data'; import {Point2D} from './primitives'; import { Attribute, PointFamily, Window, TypeOf, equals, Sort, export_to_txt, RubberBand } from './utils'; -import { PlotContour, PlotScatter, ParallelPlot, PrimitiveGroupContainer, Histogram, Frame, newScatter, BasePlot } from './subplots'; +import { PlotContour, PlotScatter, ParallelPlot, PrimitiveGroupContainer, Histogram, Frame, newScatter, BasePlot, newGraph2D } from './subplots'; import { List, Shape, MyObject } from './toolbox'; import { string_to_hex, string_to_rgb, rgb_to_string, colorHsl } from './color_conversion'; @@ -85,7 +85,7 @@ export class MultiplePlots { let object_type_ = this.dataObjects[i]['type_']; if (this.dataObjects[i]['type_'] == 'graph2d') { this.dataObjects[i]['elements'] = elements; - var newObject:any = new PlotScatter(this.dataObjects[i], this.sizes[i]['width'], this.sizes[i]['height'], buttons_ON, this.initial_coords[i][0], this.initial_coords[i][1], canvas_id, true); + var newObject:any = new newGraph2D(this.dataObjects[i], this.sizes[i]['width'], this.sizes[i]['height'], buttons_ON, this.initial_coords[i][0], this.initial_coords[i][1], canvas_id, true); } else if (object_type_ === 'parallelplot') { this.dataObjects[i]['elements'] = elements; newObject = new ParallelPlot(this.dataObjects[i], this.sizes[i]['width'], this.sizes[i]['height'], buttons_ON, this.initial_coords[i][0], this.initial_coords[i][1], canvas_id, true); @@ -382,25 +382,10 @@ export class MultiplePlots { } add_scatterplot(attr_x:Attribute, attr_y:Attribute) { - var graduation_style = {text_color:string_to_rgb('grey'), font_size:12, font_style:'sans-serif', text_align_x:'center', text_align_y:'alphabetic', name:''}; - var axis_style = {line_width:0.5, color_stroke:string_to_rgb('grey'), dashline:[], name:''}; - var DEFAULT_AXIS = {nb_points_x:10, nb_points_y:10, graduation_style: graduation_style, axis_style: axis_style, arrow_on: false, grid_on: true, type_:'axis', name:''}; - var surface_style = {color_fill: string_to_rgb('lightblue'), opacity:0.75, hatching:undefined}; - var text_style = {text_color: string_to_rgb('black'), font_size:10, font_style:'sans-serif', text_align_x:'start', text_align_y:'alphabetic', name:''}; - var DEFAULT_TOOLTIP = {attribute_names:[attr_x.name, attr_y.name], surface_style:surface_style, text_style:text_style, tooltip_radius:5, type_:'tooltip', name:''}; - var point_style = {color_fill:string_to_rgb('lightblue'), color_stroke:string_to_rgb('grey'), stroke_width:0.5, size:2, shape:'circle', name:''}; - var new_scatter = { - tooltip: DEFAULT_TOOLTIP, - attribute_names: [attr_x.name, attr_y.name], - point_style: point_style, - elements: this.data['elements'], - axis:DEFAULT_AXIS, - type_:'scatterplot', - name:'' - }; + var new_scatter = {attribute_names: [attr_x.name, attr_y.name], elements:this.data['elements'], type_:'frame', name:''}; var DEFAULT_WIDTH = 560; var DEFAULT_HEIGHT = 300; - var new_plot_data = new PlotScatter(new_scatter, DEFAULT_WIDTH, DEFAULT_HEIGHT, this.buttons_ON, 0, 0, this.canvas_id); + var new_plot_data = new newScatter(new_scatter, DEFAULT_WIDTH, DEFAULT_HEIGHT, this.buttons_ON, 0, 0, this.canvas_id); this.initialize_new_plot_data(new_plot_data); } @@ -628,15 +613,18 @@ export class MultiplePlots { if (List.is_include(plotIndex, this.to_display_plots)) { if (plot.type_ == 'parallelplot') { plot.refresh_axis_coords() } if (plot instanceof BasePlot) { - plot.selectedIndices = this.selectedIndices; - plot.clickedIndices = [...this.clickedIndices]; - plot.hoveredIndices = [...this.hoveredIndices]; - if (plot instanceof Frame) { - if (this.point_families.length != 0) { - plot.pointSetColors = this.point_families.map((pointFamily, familyIdx) => { - pointFamily.pointIndices.forEach(pointIdx => plot.pointSets[pointIdx] = familyIdx); - return pointFamily.color - }) + // TODO: not so beautiful but here to avoid selecting points with unlinked graph points + if ( !(plot instanceof newGraph2D) ) { + plot.selectedIndices = this.selectedIndices; + plot.clickedIndices = [...this.clickedIndices]; + plot.hoveredIndices = [...this.hoveredIndices]; + if (plot instanceof Frame) { + if (this.point_families.length != 0) { + plot.pointSetColors = this.point_families.map((pointFamily, familyIdx) => { + pointFamily.pointIndices.forEach(pointIdx => plot.pointSets[pointIdx] = familyIdx); + return pointFamily.color + }) + } } } } else if (plot instanceof ParallelPlot) { @@ -921,10 +909,13 @@ export class MultiplePlots { this.selectedIndices = List.listIntersection(this.selectedIndices, obj.pp_selected_index); } else if (obj instanceof BasePlot) { obj.axes.forEach(axis => { - if (axis.rubberBand.length != 0) { - isSelecting = true; - const selectedIndices = (obj as BasePlot).updateSelected(axis); - this.selectedIndices = List.listIntersection(this.selectedIndices, selectedIndices); + // TODO: not so beautiful but here to avoid selecting points with unlinked graph points + if ( !(obj instanceof newGraph2D) ) { + if (axis.rubberBand.length != 0) { + isSelecting = true; + const selectedIndices = (obj as BasePlot).updateSelected(axis); + this.selectedIndices = List.listIntersection(this.selectedIndices, selectedIndices); + } } }) } @@ -1843,7 +1834,9 @@ export class MultiplePlots { this.objectList.forEach(plot => { if (plot instanceof BasePlot) plot.switchSelection() }); } - public switchMerge() { this.objectList.forEach(plot => { if (plot instanceof newScatter) plot.switchMerge() })}; + public switchMerge() { this.objectList.forEach(plot => { if (plot instanceof BasePlot) plot.switchMerge() })}; + + public togglePoints() { this.objectList.forEach(plot => { if (plot instanceof BasePlot) plot.togglePoints() })}; public switchZoom() { this.isZooming = !this.isZooming; @@ -1872,7 +1865,7 @@ export class MultiplePlots { this.redrawAllObjects(); } - mouse_interaction(): void { + mouse_interaction(): void { //TODO: this has to be totally refactored, with special behaviors defined in each plot class var mouse1X:number = 0; var mouse1Y:number = 0; var mouse2X:number = 0; var mouse2Y:number = 0; var mouse3X:number = 0; var mouse3Y:number = 0; var isDrawing = false; var mouse_moving:boolean = false; @@ -1969,6 +1962,7 @@ export class MultiplePlots { this.manage_mouse_interactions(mouse2X, mouse2Y); if (!this.isZooming) { + const currentPlot = this.objectList[this.last_index]; if (isDrawing) { mouse_moving = true; if (this.selectDependency_bool) { @@ -1976,7 +1970,8 @@ export class MultiplePlots { this.mouse_move_pp_communication(); this.mouse_move_frame_communication(); this.refreshRubberBands(); - this.updateSelectedPrimitives(); + // TODO: not so beautiful but here to avoid selecting points with unlinked graph points + if ( !(currentPlot instanceof newGraph2D) ) this.updateSelectedPrimitives(); this.redrawAllObjects(); } this.redraw_object(); @@ -1984,9 +1979,9 @@ export class MultiplePlots { if (this.selectDependency_bool) { this.mouse_over_primitive_group(); this.mouse_over_scatter_plot(); - if (this.objectList[this.last_index] instanceof BasePlot) { - this.hoveredIndices = (this.objectList[this.last_index] as BasePlot).hoveredIndices; - this.clickedIndices = (this.objectList[this.last_index] as BasePlot).clickedIndices; + if (currentPlot instanceof BasePlot && !(currentPlot instanceof newGraph2D)) { + this.hoveredIndices = (currentPlot as BasePlot).hoveredIndices; + this.clickedIndices = (currentPlot as BasePlot).clickedIndices; } this.redrawAllObjects(); } @@ -2009,7 +2004,7 @@ export class MultiplePlots { this.click_on_button_action(click_on_manip_button, click_on_selectDep_button, click_on_view, click_on_export); } - if (mouse_moving === false) { + if (!mouse_moving) { if (this.selectDependency_bool) { if (this.clickedPlotIndex !== -1) { let type_ = this.objectList[this.clickedPlotIndex].type_ @@ -2034,7 +2029,7 @@ export class MultiplePlots { this.pp_communication(this.objectList[this.clickedPlotIndex].rubber_bands, this.objectList[this.clickedPlotIndex]); } } - if (this.objectList[this.last_index] instanceof BasePlot) { + if (this.objectList[this.last_index] instanceof BasePlot && !(this.objectList[this.last_index] instanceof newGraph2D)) { this.hoveredIndices = (this.objectList[this.last_index] as BasePlot).hoveredIndices; this.clickedIndices = (this.objectList[this.last_index] as BasePlot).clickedIndices; } @@ -2045,7 +2040,8 @@ export class MultiplePlots { } this.refreshRubberBands(); this.manage_selected_point_index_changes(old_selected_index); - this.updateSelectedPrimitives(); + // TODO: not so beautiful but here to avoid selecting points with unlinked graph points + if ( !(this.objectList[this.last_index] instanceof newGraph2D) ) this.updateSelectedPrimitives(); if (!shiftKey) this.isSelecting = false; this.isZooming = false; this.objectList.forEach(plot => { diff --git a/src/plot-data.ts b/src/plot-data.ts index 1abfc215..0796255d 100644 --- a/src/plot-data.ts +++ b/src/plot-data.ts @@ -1,6 +1,6 @@ import { heatmap_color, string_to_hex } from "./color_conversion"; import { Point2D, PrimitiveGroup, Contour2D, Circle2D, Dataset, Graph2D, Scatter, Heatmap, Wire } from "./primitives"; -import { Attribute, PointFamily, Axis, Tooltip, Sort, permutator, export_to_csv, RubberBand, newText, textParams, Vertex, newRect } from "./utils"; +import { Attribute, PointFamily, Axis, Tooltip, Sort, permutator, export_to_csv, RubberBand, newText, TextParams, Vertex, newRect } from "./utils"; import { EdgeStyle } from "./style"; import { Shape, List, MyMath } from "./toolbox"; import { colorHex, tint_rgb, hex_to_rgb, rgb_to_string, get_interpolation_colors, rgb_strToVector } from "./color_conversion"; @@ -851,7 +851,7 @@ export abstract class PlotData extends EventEmitter { let origin = new Vertex(current_x, this.axis_y_end - 10); let width = this.x_step * 0.95; let align = "center"; - const textParams: textParams = { + const textParams: TextParams = { width: width, height: origin.y - 2 - this.Y, align: align, baseline: "bottom", multiLine: true, color: 'black', backgroundColor: "hsla(0, 0%, 100%, 0.5)" }; let axisTitle = new newText(this.axis_list[i]['name'], origin, textParams); @@ -971,7 +971,7 @@ export abstract class PlotData extends EventEmitter { Shape.drawLine(this.context, [[this.axis_x_start, current_y], [this.axis_x_end, current_y]]); let origin = new Vertex(this.axis_x_start, current_y + 10); - const textParams: textParams = { + const textParams: TextParams = { width: this.width * 0.25, height: this.y_step * 0.98, align: "center", baseline: "hanging", multiLine: true, color: 'black', backgroundColor: "hsla(0, 0%, 100%, 0.5)" }; let axisTitle = new newText(this.axis_list[i]['name'], origin, textParams); diff --git a/src/subplots.ts b/src/subplots.ts index 6dd94ebb..b9e04436 100644 --- a/src/subplots.ts +++ b/src/subplots.ts @@ -1,5 +1,6 @@ import { PlotData, Buttons, Interactions } from "./plot-data"; -import { Attribute, Axis, Sort, set_default_values, TypeOf, RubberBand, Vertex, newAxis, ScatterPoint, Bar, DrawingCollection, SelectionBox, GroupCollection } from "./utils"; +import { Attribute, Axis, Sort, set_default_values, TypeOf, RubberBand, Vertex, newAxis, ScatterPoint, Bar, DrawingCollection, SelectionBox, GroupCollection, + LineSequence, newRect, newPointStyle } from "./utils"; import { Heatmap, PrimitiveGroup } from "./primitives"; import { List, Shape, MyObject } from "./toolbox"; import { Graph2D, Scatter } from "./primitives"; @@ -1472,6 +1473,7 @@ export class BasePlot extends PlotData { public nSamples: number; public pointSets: number[]; public pointSetColors: string[] = []; + public pointStyles: newPointStyle[] = null; public isSelecting: boolean = false; public selectionBox = new SelectionBox(); @@ -1501,10 +1503,10 @@ export class BasePlot extends PlotData { public is_in_multiplot: boolean = false ) { super(data, width, height, buttons_ON, X, Y, canvas_id, is_in_multiplot); - this.nSamples = data.elements.length; this.origin = new Vertex(0, 0); this.size = new Vertex(width - X, height - Y); this.features = this.unpackData(data); + this.nSamples = this.features.entries().next().value[1].length; // a little bit cumbersome this.initSelectors(); this.scaleX = this.scaleY = 1; this.TRL_THRESHOLD /= Math.min(Math.abs(this.initScale.x), Math.abs(this.initScale.y)); @@ -1532,14 +1534,14 @@ export class BasePlot extends PlotData { get falseIndicesArray(): boolean[] { return new Array(this.nSamples).fill(false) } - public unpackAxisStyle(data:any): void { + protected unpackAxisStyle(data:any): void { if (data.axis?.axis_style?.color_stroke) this.axisStyle.set("strokeStyle", data.axis.axis_style.color_stroke); if (data.axis?.axis_style?.line_width) this.axisStyle.set("lineWidth", data.axis.axis_style.line_width); if (data.axis?.graduation_style?.font_style) this.axisStyle.set("font", data.axis.graduation_style.font_style); if (data.axis?.graduation_style?.font_size) this.axisStyle.set("ticksFontsize", data.axis.graduation_style.font_size); } - private unpackData(data: any): Map { + protected unpackData(data: any): Map { const featuresKeys: string[] = Array.from(Object.keys(data.elements[0].values)); featuresKeys.push("name"); let unpackedData = new Map(); @@ -1547,7 +1549,7 @@ export class BasePlot extends PlotData { return unpackedData } - public drawCanvas(): void { + private drawCanvas(): void { this.context = this.context_show; this.draw_empty_canvas(this.context_show); if (this.settings_on) this.draw_settings_rect() @@ -1563,6 +1565,10 @@ export class BasePlot extends PlotData { axis.update(this.axisStyle, this.viewPoint, new Vertex(this.scaleX, this.scaleY), this.translation); if (axis.rubberBand.length != 0) axesSelections.push(this.updateSelected(axis)); }) + this.updateSelection(axesSelections); + } + + public updateSelection(axesSelections: number[][]): void { if (!this.is_in_multiplot) this.selectedIndices = BasePlot.intersectArrays(axesSelections); } @@ -1578,32 +1584,34 @@ export class BasePlot extends PlotData { return arraysIntersection } - public updateSize(): void { this.size = new Vertex(this.width, this.height) } + protected updateSize(): void { this.size = new Vertex(this.width, this.height) } - public resetAxes(): void { this.axes.forEach(axis => axis.reset()) } + protected resetAxes(): void { this.axes.forEach(axis => axis.reset()) } - public reset_scales(): void { + public reset_scales(): void { // TODO: merge with resetView this.updateSize(); this.axes.forEach(axis => axis.resetScale()); } + public resetView(): void { this.reset_scales(); this.draw() } + public initSelectors(): void { this.hoveredIndices = []; this.clickedIndices = []; this.selectedIndices = []; } - public resetSelectors(): void { + protected resetSelectors(): void { this.selectionBox = new SelectionBox(); this.initSelectors(); } - public reset(): void { + protected reset(): void { this.resetAxes(); this.resetSelectors(); } - public resetSelection(): void { + protected resetSelection(): void { this.axes.forEach(axis => axis.rubberBand.reset()); this.resetSelectors(); } @@ -1615,27 +1623,27 @@ export class BasePlot extends PlotData { return selection } - public isRubberBanded(): boolean { + protected isRubberBanded(): boolean { let isRubberBanded = true; this.axes.forEach(axis => isRubberBanded = isRubberBanded && axis.rubberBand.length != 0); return isRubberBanded } - public drawAxes(): void { this.axes.forEach(axis => axis.draw(this.context_show)) } + protected drawAxes(): void { this.axes.forEach(axis => axis.draw(this.context_show)) } - public drawZoneRectangle(context: CanvasRenderingContext2D): void { + private drawZoneRectangle(context: CanvasRenderingContext2D): void { // TODO: change with newRect Shape.rect(this.X, this.Y, this.width, this.height, context, "hsl(203, 90%, 88%)", "hsl(0, 0%, 0%)", 1, 0.3, [15,15]); } - public drawRelativeObjects() {} + protected drawRelativeObjects() {} - public drawAbsoluteObjects(context: CanvasRenderingContext2D) { + protected drawAbsoluteObjects(context: CanvasRenderingContext2D) { this.absoluteObjects = new GroupCollection(); this.drawSelectionBox(context); } - public computeRelativeObjects() {} + protected computeRelativeObjects() {} public draw(): void { this.context_show.save(); @@ -1674,13 +1682,17 @@ export class BasePlot extends PlotData { public switchSelection(): void { this.isSelecting = !this.isSelecting; this.draw() } + public switchMerge(): void {} + public switchZoom(): void { this.isZooming = !this.isZooming } - public updateSelectionBox(frameDown: Vertex, frameMouse: Vertex): void { this.selectionBox.update(frameDown, frameMouse) } + public togglePoints(): void {} + + protected updateSelectionBox(frameDown: Vertex, frameMouse: Vertex): void { this.selectionBox.update(frameDown, frameMouse) } public get drawingZone(): [Vertex, Vertex] { return [new Vertex(this.X, this.Y), this.size] } - public drawSelectionBox(context: CanvasRenderingContext2D) { + protected drawSelectionBox(context: CanvasRenderingContext2D) { if ((this.isSelecting || this.is_drawing_rubber_band) && this.selectionBox.isDefined) { const [drawingOrigin, drawingSize] = this.drawingZone; this.selectionBox.buildRectFromHTMatrix(drawingOrigin, drawingSize, this.relativeMatrix); @@ -1692,7 +1704,7 @@ export class BasePlot extends PlotData { } } - public drawZoomBox(zoomBox: SelectionBox, frameDown: Vertex, frameMouse: Vertex, context: CanvasRenderingContext2D): void { + private drawZoomBox(zoomBox: SelectionBox, frameDown: Vertex, frameMouse: Vertex, context: CanvasRenderingContext2D): void { zoomBox.update(frameDown, frameMouse); const [drawingOrigin, drawingSize] = this.drawingZone; zoomBox.buildRectFromHTMatrix(drawingOrigin, drawingSize, this.relativeMatrix); @@ -1700,7 +1712,7 @@ export class BasePlot extends PlotData { zoomBox.draw(context); } - public zoomBoxUpdateAxes(zoomBox: SelectionBox): void { // TODO: will not work for a 3+ axes plot + protected zoomBoxUpdateAxes(zoomBox: SelectionBox): void { // TODO: will not work for a 3+ axes plot this.axes[0].minValue = Math.min(zoomBox.minVertex.x, zoomBox.maxVertex.x); this.axes[0].maxValue = Math.max(zoomBox.minVertex.x, zoomBox.maxVertex.x); this.axes[1].minValue = Math.min(zoomBox.minVertex.y, zoomBox.maxVertex.y); @@ -1709,12 +1721,12 @@ export class BasePlot extends PlotData { this.updateAxes(); } - public drawTooltips(): void { + private drawTooltips(): void { this.relativeObjects.drawTooltips(new Vertex(this.X, this.Y), this.size, this.context_show, this.is_in_multiplot); this.absoluteObjects.drawTooltips(new Vertex(this.X, this.Y), this.size, this.context_show, this.is_in_multiplot); } - public stateUpdate(context: CanvasRenderingContext2D, canvasMouse: Vertex, absoluteMouse: Vertex, + protected stateUpdate(context: CanvasRenderingContext2D, canvasMouse: Vertex, absoluteMouse: Vertex, frameMouse: Vertex, stateName: string, keepState: boolean, invertState: boolean): void { this.fixedObjects.updateMouseState(context, canvasMouse, stateName, keepState, invertState); this.absoluteObjects.updateMouseState(context, absoluteMouse, stateName, keepState, invertState); @@ -1846,7 +1858,7 @@ export class BasePlot extends PlotData { } } - public drawAfterRescale(mouse3X: number, mouse3Y: number, scale: Vertex): void { + private drawAfterRescale(mouse3X: number, mouse3Y: number, scale: Vertex): void { for (let axis of this.axes) { if (axis.tickPrecision >= this.MAX_PRINTED_NUMBERS) { if (this.scaleX > scale.x) {this.scaleX = scale.x} @@ -1868,7 +1880,7 @@ export class BasePlot extends PlotData { public zoomOut(): void { this.zoom(new Vertex(this.X + this.size.x / 2, this.Y + this.size.y / 2), -342) } - public zoom(center: Vertex, zFactor: number): void { + private zoom(center: Vertex, zFactor: number): void { const [mouse3X, mouse3Y] = this.wheel_interaction(center.x, center.y, zFactor); this.drawAfterRescale(mouse3X, mouse3Y, new Vertex(1, 1)); } @@ -1974,7 +1986,7 @@ export class Frame extends BasePlot { return [origin.transform(this.canvasMatrix.inverse()), size] } - public unpackAxisStyle(data: any): void { + protected unpackAxisStyle(data: any): void { if (data.axis) { super.unpackAxisStyle(data); this.nXTicks = data.axis.nb_points_x; @@ -1982,7 +1994,7 @@ export class Frame extends BasePlot { } } - public stateUpdate(context: CanvasRenderingContext2D, canvasMouse: Vertex, absoluteMouse: Vertex, + protected stateUpdate(context: CanvasRenderingContext2D, canvasMouse: Vertex, absoluteMouse: Vertex, frameMouse: Vertex, stateName: string, keepState: boolean, invertState: boolean): void { super.stateUpdate(context, canvasMouse, absoluteMouse, frameMouse, stateName, keepState, invertState); if (stateName == "isHovered") this.hoveredIndices = this.sampleDrawings.updateSampleStates(stateName); @@ -1996,7 +2008,7 @@ export class Frame extends BasePlot { return new Vertex(Math.max(naturalOffset.x, calibratedMeasure), Math.max(naturalOffset.y, MIN_FONTSIZE)); } - public updateSize(): void { this.size = new Vertex(this.width, this.height) } + protected updateSize(): void { this.size = new Vertex(this.width, this.height) } public reset_scales(): void { this.updateSize(); @@ -2010,27 +2022,41 @@ export class Frame extends BasePlot { this.axes[1].transform(frameOrigin, yEnd); } - public setFeatures(data: any): [string, string] { - return [data.attribute_names[0], data.attribute_names[1]]; + protected setFeatures(data: any): [string, string] { + let xFeature = data.attribute_names[0]; + let yFeature = data.attribute_names[1]; + if (!xFeature) { + xFeature = "indices"; + this.features.set("indices", Array.from(Array(this.nSamples).keys())); + } + if (!yFeature) { + for (let key of Array.from(this.features.keys())) { + if (!["name", "indices"].includes(key)) { + yFeature = key; + break; + } + } + } + return [xFeature, yFeature] } - public setAxes(): newAxis[] { + protected setAxes(): newAxis[] { const [frameOrigin, xEnd, yEnd, freeSize] = this.setFrameBounds() return [ this.setAxis(this.xFeature, freeSize.y, frameOrigin, xEnd, this.nXTicks), this.setAxis(this.yFeature, freeSize.x, frameOrigin, yEnd, this.nYTicks)] } - public setAxis(feature: string, freeSize: number, origin: Vertex, end: Vertex, nTicks: number = undefined): newAxis { + protected setAxis(feature: string, freeSize: number, origin: Vertex, end: Vertex, nTicks: number = undefined): newAxis { return new newAxis(this.features.get(feature), freeSize, origin, end, feature, this.initScale, nTicks) } - public drawAxes() { + protected drawAxes() { super.drawAxes(); if (this.isRubberBanded()) this.updateSelectionBox(...this.rubberBandsCorners); } - public setFrameBounds(): [Vertex, Vertex, Vertex, Vertex] { + protected setFrameBounds(): [Vertex, Vertex, Vertex, Vertex] { let frameOrigin = this.offset.add(new Vertex(this.X, this.Y).scale(this.initScale)); let xEnd = new Vertex(this.size.x - this.margin.x + this.X * this.initScale.x, frameOrigin.y); let yEnd = new Vertex(frameOrigin.x, this.size.y - this.margin.y + this.Y * this.initScale.y); @@ -2050,7 +2076,7 @@ export class Frame extends BasePlot { return [frameOrigin, xEnd, yEnd, freeSize] } - public updateSelectionBox(frameDown: Vertex, frameMouse: Vertex) { + protected updateSelectionBox(frameDown: Vertex, frameMouse: Vertex) { this.axes[0].rubberBand.minValue = Math.min(frameDown.x, frameMouse.x); this.axes[1].rubberBand.minValue = Math.min(frameDown.y, frameMouse.y); this.axes[0].rubberBand.maxValue = Math.max(frameDown.x, frameMouse.x); @@ -2067,7 +2093,6 @@ export class Frame extends BasePlot { this.is_drawing_rubber_band = true; this.selectionBox.rubberBandUpdate(e, ["x", "y"][index]); })); - super.mouse_interaction(isParallelPlot); } } @@ -2102,7 +2127,7 @@ export class Histogram extends Frame { set nYTicks(value: number) {this._nYTicks = value} - public unpackAxisStyle(data: any): void { + protected unpackAxisStyle(data: any): void { super.unpackAxisStyle(data); if (data.graduation_nb) this.nXTicks = data.graduation_nb; } @@ -2114,7 +2139,7 @@ export class Histogram extends Frame { if (data.edge_style?.dashline) this.dashLine = data.edge_style.dashline; } - public reset(): void { + protected reset(): void { super.reset(); this.bars = []; } @@ -2165,18 +2190,18 @@ export class Histogram extends Frame { return bars } - public computeRelativeObjects(): void { + protected computeRelativeObjects(): void { this.bars = this.computeBars(this.axes[0], this.features.get(this.xFeature)); this.axes[1] = this.updateNumberAxis(this.axes[1], this.bars); this.getBarsDrawing(); } - public drawRelativeObjects(): void { + protected drawRelativeObjects(): void { this.bars.forEach(bar => { bar.buildPath() ; bar.draw(this.context_show) }); this.relativeObjects = new GroupCollection([...this.bars], this.relativeMatrix); } - public getBarsDrawing(): void { + private getBarsDrawing(): void { const fullTicks = this.boundedTicks(this.axes[0]); const minY = this.boundedTicks(this.axes[1])[0]; this.bars.forEach((bar, index) => { @@ -2195,7 +2220,7 @@ export class Histogram extends Frame { }) } - public setAxes(): newAxis[] { + protected setAxes(): newAxis[] { const [frameOrigin, xEnd, yEnd, freeSize] = this.setFrameBounds(); const xAxis = this.setAxis(this.xFeature, freeSize.y, frameOrigin, xEnd, this.nXTicks); const bars = this.computeBars(xAxis, this.features.get(this.xFeature)); @@ -2204,13 +2229,13 @@ export class Histogram extends Frame { return [xAxis, yAxis]; } - public setFeatures(data: any): [string, string] { return [data.x_variable, 'number'] } + protected setFeatures(data: any): [string, string] { return [data.x_variable, 'number'] } public mouseTranslate(currentMouse: Vertex, mouseDown: Vertex): Vertex { return new Vertex(this.axes[0].isDiscrete ? 0 : currentMouse.x - mouseDown.x, 0) } - wheel_interaction(mouse3X: number, mouse3Y: number, deltaY: number): [number, number] { // TODO: REALLY NEEDS A REFACTOR + public wheel_interaction(mouse3X: number, mouse3Y: number, deltaY: number): [number, number] { // TODO: REALLY NEEDS A REFACTOR // e.preventDefault(); this.fusion_coeff = 1.2; if (!this.axes[0].isDiscrete) { @@ -2241,8 +2266,8 @@ const DEFAULT_POINT_COLOR: string = 'hsl(203, 90%, 85%)'; export class newScatter extends Frame { public points: ScatterPoint[] = []; - public fillStyle: string; - public strokeStyle: string; + public fillStyle: string = DEFAULT_POINT_COLOR; + public strokeStyle: string = null; public marker: string = 'circle'; public pointSize: number = 8; public lineWidth: number = 1; @@ -2262,7 +2287,7 @@ export class newScatter extends Frame { public is_in_multiplot: boolean = false ) { super(data, width, height, buttons_ON, X, Y, canvas_id, is_in_multiplot); - if (!data.tooltip) {this.tooltipAttributes = Array.from(this.features.keys()) } + if (!data.tooltip) this.tooltipAttributes = Array.from(this.features.keys()); else this.tooltipAttributes = data.tooltip.attribute; this.unpackPointStyle(data); this.computePoints(); @@ -2280,7 +2305,7 @@ export class newScatter extends Frame { if (data.points_sets) this.unpackPointsSets(data); } - public unpackPointsSets(data: any): void { + private unpackPointsSets(data: any): void { data.points_sets.forEach((pointSet, setIndex) => { pointSet.point_index.forEach(pointIndex => { this.pointSets[pointIndex] = setIndex; @@ -2294,19 +2319,21 @@ export class newScatter extends Frame { this.computePoints(); } - public reset(): void { + protected reset(): void { super.reset(); this.computePoints(); this.resetClusters(); } - public drawAbsoluteObjects(context: CanvasRenderingContext2D): void { + protected drawAbsoluteObjects(context: CanvasRenderingContext2D): void { this.drawPoints(context); this.absoluteObjects = new GroupCollection([...this.points]); this.drawSelectionBox(context); }; - public drawPoints(context: CanvasRenderingContext2D): void { + protected drawPoints(context: CanvasRenderingContext2D): void { + const axesOrigin = this.axes[0].origin; + const axesEnd = new Vertex(this.axes[0].end.x, this.axes[1].end.y); this.points.forEach(point => { let color = this.fillStyle; const colors = new Map(); @@ -2327,9 +2354,15 @@ export class newScatter extends Frame { }; point.lineWidth = this.lineWidth; point.setColors(color); - point.marker = this.marker; + if (this.pointStyles) { + if (!this.clusterColors) point.updateStyle(this.pointStyles[point.values[0]]) + else { + let clusterPointStyle = Object.assign({}, this.pointStyles[point.values[0]], { strokeStyle: null }); + point.updateStyle(clusterPointStyle); + } + } else point.marker = this.marker; point.update(); - if (point.isInFrame(this.axes[0], this.axes[1])) point.draw(context); + if (point.isInFrame(axesOrigin, axesEnd, this.initScale)) point.draw(context); }) } @@ -2349,7 +2382,7 @@ export class newScatter extends Frame { this.draw(); } - public zoomBoxUpdateAxes(zoomBox: SelectionBox): void { // TODO: will not work for a 3+ axes plot + protected zoomBoxUpdateAxes(zoomBox: SelectionBox): void { // TODO: will not work for a 3+ axes plot super.zoomBoxUpdateAxes(zoomBox); this.computePoints(); } @@ -2502,7 +2535,7 @@ export class newScatter extends Frame { public mouseDown(canvasMouse: Vertex, frameMouse: Vertex, absoluteMouse: Vertex): [Vertex, Vertex, any] { let [superCanvasMouse, superFrameMouse, clickedObject] = super.mouseDown(canvasMouse, frameMouse, absoluteMouse); - this.previousCoords = this.points.map(p => p.center) + this.previousCoords = this.points.map(p => p.center); return [superCanvasMouse, superFrameMouse, clickedObject] } @@ -2533,6 +2566,101 @@ export class newScatter extends Frame { } } +export class newGraph2D extends newScatter { + public curves: LineSequence[]; + private curvesIndices: number[][]; + public showPoints: boolean = false; + constructor( + data: any, + public width: number, + public height: number, + public buttons_ON: boolean, + public X: number, + public Y: number, + public canvas_id: string, + public is_in_multiplot: boolean = false + ) { + super(data, width, height, buttons_ON, X, Y, canvas_id, is_in_multiplot); + } + + protected unpackData(data: any): Map { + const graphSamples = []; + this.pointStyles = []; + this.curvesIndices = []; + this.curves = []; + if (data.graphs) { + data.graphs.forEach(graph => { + if (graph.elements.length != 0) { + this.curves.push(LineSequence.getGraphProperties(graph)); + const curveIndices = range(graphSamples.length, graphSamples.length + graph.elements.length); + const graphPointStyle = new newPointStyle(graph.point_style); + this.pointStyles.push(...new Array(curveIndices.length).fill(graphPointStyle)); + this.curvesIndices.push(curveIndices); + graphSamples.push(...graph.elements); + } + }) + } + return super.unpackData({"elements": graphSamples}) + } + + public updateSelection(axesSelections: number[][]): void { this.selectedIndices = BasePlot.intersectArrays(axesSelections) } + + public drawCurves(context: CanvasRenderingContext2D): void { + const axesOrigin = this.axes[0].origin.transform(this.canvasMatrix); + const axesEnd = new Vertex(this.axes[0].end.x, this.axes[1].end.y).transform(this.canvasMatrix); + const drawingZone = new newRect(axesOrigin, axesEnd.subtract(axesOrigin)); + const previousCanvas = context.getImageData(0, 0, context.canvas.width, context.canvas.height); + this.curves.forEach((curve, curveIndex) => { + curve.update(this.curvesIndices[curveIndex].map(index => { return this.points[index] })); + curve.draw(context); + }) + context.globalCompositeOperation = "destination-in"; + context.fill(drawingZone.path); + const cutGraph = context.getImageData(this.X, this.Y, this.size.x, this.size.y); + context.globalCompositeOperation = "source-over"; + context.putImageData(previousCanvas, 0, 0); + context.putImageData(cutGraph, this.X, this.Y); + } + + protected drawAbsoluteObjects(context: CanvasRenderingContext2D): void { + this.drawCurves(context); + this.absoluteObjects = new GroupCollection([...this.curves]); + if (this.showPoints) { + this.drawPoints(context); + this.absoluteObjects.drawings = [...this.points, ...this.absoluteObjects.drawings]; + } + this.drawSelectionBox(context); + }; + + public reset_scales(): void { + const scale = new Vertex(this.frameMatrix.a, this.frameMatrix.d).scale(this.initScale); + const translation = new Vertex(this.axes[0].maxValue - this.axes[0].initMaxValue, this.axes[1].maxValue - this.axes[1].initMaxValue).scale(scale); + this.curves.forEach(curve => curve.translateTooltip(translation)); + super.reset_scales(); + } + + public switchMerge(): void { this.isMerged = false } + + public togglePoints(): void { this.showPoints = !this.showPoints; this.draw() } + + public mouseUp(canvasMouse: Vertex, frameMouse: Vertex, absoluteMouse: Vertex, canvasDown: Vertex, ctrlKey: boolean): void { + super.mouseUp(canvasMouse, frameMouse, absoluteMouse, canvasDown, ctrlKey); + this.curves.forEach(curve => curve.previousTooltipOrigin = curve.tooltipOrigin); + } + + public mouseTranslate(currentMouse: Vertex, mouseDown: Vertex): Vertex { + const translation = super.mouseTranslate(currentMouse, mouseDown); + this.curves.forEach(curve => { if (curve.previousTooltipOrigin) curve.tooltipOrigin = curve.previousTooltipOrigin.add(translation.scale(this.initScale)) }); + return translation + } +} + +function range(start: number, end: number, step: number = 1): number[] { + let array = []; + for (let i = start; i < end; i = i + step) array.push(i); + return array +} + function mean(array: number[]): number { let sum = 0; array.forEach(value => sum += value); diff --git a/src/utils.ts b/src/utils.ts index c96ec20c..d5f996fc 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1297,6 +1297,13 @@ export class Vertex { return copy } + public translate(translation: Vertex): Vertex { return this.add(translation) } + + public translateSelf(translation: Vertex): void { + this.x += translation.x; + this.y += translation.y; + } + public subtract(other: Vertex): Vertex { let copy = this.copy(); copy.x = this.x - other.x; @@ -1334,13 +1341,12 @@ export class newShape { public isClicked: boolean = false; public isSelected: boolean = false; public isScaled: boolean = true; + public isFilled: boolean = true; public inFrame: boolean = true; + public onFrame: boolean = false; public tooltipOrigin: Vertex; protected _tooltipMap = new Map(); - - protected readonly TOOLTIP_SURFACE: SurfaceStyle = new SurfaceStyle(string_to_hex("lightgrey"), 0.5, null); - protected readonly TOOLTIP_TEXT_STYLE: TextStyle = new TextStyle(string_to_hex("black"), 14, "Calibri"); constructor() {}; get tooltipMap(): Map { return this._tooltipMap }; @@ -1358,17 +1364,26 @@ export class newShape { context.scale(1 / contextMatrix.a, 1 / contextMatrix.d); } else scaledPath.addPath(this.path); this.setDrawingProperties(context); - context.fill(scaledPath); + if (this.isFilled) context.fill(scaledPath); context.stroke(scaledPath); context.restore(); } + public setStrokeStyle(fillStyle: string): string { + const [h, s, l] = hslToArray(colorHsl(fillStyle)); + const lValue = l <= STROKE_STYLE_OFFSET ? l + STROKE_STYLE_OFFSET : l - STROKE_STYLE_OFFSET; + return `hsl(${h}, ${s}%, ${lValue}%)`; + } + public setDrawingProperties(context: CanvasRenderingContext2D) { context.lineWidth = this.lineWidth; context.strokeStyle = this.strokeStyle; context.setLineDash(this.dashLine); context.globalAlpha = this.alpha; - context.fillStyle = this.isHovered ? this.hoverStyle : this.isClicked ? this.clickedStyle : this.isSelected ? this.selectedStyle : this.fillStyle; + if (this.isFilled) { + context.fillStyle = this.isHovered ? this.hoverStyle : this.isClicked ? this.clickedStyle : this.isSelected ? this.selectedStyle : this.fillStyle; + context.strokeStyle = this.setStrokeStyle(context.fillStyle); + } else context.strokeStyle = this.isHovered ? this.hoverStyle : this.isClicked ? this.clickedStyle : this.isSelected ? this.selectedStyle : this.strokeStyle; } public initTooltip(context: CanvasRenderingContext2D): newTooltip { return new newTooltip(this.tooltipOrigin, this.tooltipMap, context) } @@ -1395,13 +1410,12 @@ export class newCircle extends newShape { public radius: number = 1 ) { super(); - this.path = this.buildPath(); + this.buildPath(); } - public buildPath(): Path2D { - const path = new Path2D(); - path.arc(this.center.x, this.center.y, this.radius, 0, 2 * Math.PI); - return path + public buildPath(): void { + this.path = new Path2D(); + this.path.arc(this.center.x, this.center.y, this.radius, 0, 2 * Math.PI); } } @@ -1438,9 +1452,7 @@ export class newRoundRect extends newRect { const vLength = this.origin.y + this.size.y; this.path.moveTo(this.origin.x + this.radius, this.origin.y); this.path.lineTo(hLength - this.radius, this.origin.y); - this.path.quadraticCurveTo(hLength, this.origin.y, hLength, this.origin.y + this.radius); - this.path.lineTo(hLength, this.origin.y + this.size.y - this.radius); this.path.quadraticCurveTo(hLength, vLength, hLength - this.radius, vLength); this.path.lineTo(this.origin.x + this.radius, vLength); @@ -1456,6 +1468,7 @@ export class Mark extends newShape { public size: number = 1 ) { super(); + this.isFilled = false; this.buildPath(); } @@ -1476,9 +1489,9 @@ export abstract class AbstractHalfLine extends newShape { public orientation: string = 'up' ) { super(); + this.isFilled = false; this.buildPath(); } - // public abstract buildPath(): void; } export class UpHalfLine extends AbstractHalfLine { @@ -1491,7 +1504,7 @@ export class UpHalfLine extends AbstractHalfLine { } export class DownHalfLine extends AbstractHalfLine { - public buildPath(): void { + public buildPath(): void { this.path = new Path2D(); const halfSize = this.size / 2; this.path.moveTo(this.center.x, this.center.y); @@ -1542,6 +1555,7 @@ export class Cross extends newShape { public size: number = 1 ) { super(); + this.isFilled = false; this.buildPath(); } @@ -1630,9 +1644,18 @@ export class Triangle extends AbstractTriangle { } } -export interface textParams { - width?: number, height?: number, fontsize?: number, multiLine?: boolean, font?: string, align?: string, - baseline?: string, style?: string, orientation?: number, backgroundColor?: string, color?: string +export interface TextParams { + width?: number, + height?: number, + fontsize?: number, + multiLine?: boolean, + font?: string, + align?: string, + baseline?: string, + style?: string, + orientation?: number, + backgroundColor?: string, + color?: string } const DEFAULT_FONTSIZE = 12; @@ -1664,7 +1687,7 @@ export class newText extends newShape { orientation = 0, color = "hsl(0, 0%, 0%)", backgroundColor = "hsla(0, 0%, 100%, 0)" - }: textParams = {}) { + }: TextParams = {}) { super(); this.width = width; this.height = height; @@ -1833,6 +1856,38 @@ export class newText extends newShape { } } +export interface PointStyleInterface { + size?: number, + color_fill?: string, + color_stroke?: string, + stroke_width?: number, + shape?: string, + name?: string +} + +export class newPointStyle implements PointStyleInterface { + public size: number; + public fillStyle: string; + public strokeStyle: string; + public marker: string; + public lineWidth: number; + constructor( + { size = null, + color_fill = null, + color_stroke = null, + stroke_width = null, + shape = 'circle', + name = '' + }: PointStyleInterface = {} + ) { + this.size = size; + this.fillStyle = color_fill; + this.strokeStyle = color_stroke; + this.marker = shape; + this.lineWidth = stroke_width; + } +} + const CIRCLES = ['o', 'circle', 'round']; const MARKERS = ['+', 'crux', 'mark']; const CROSSES = ['x', 'cross', 'oblique']; @@ -1860,15 +1915,30 @@ export class newPoint2D extends newShape { this.lineWidth = 1; }; - public setStrokeStyle(fillStyle: string): string { - const [h, s, l] = hslToArray(colorHsl(fillStyle)); - const lValue = l <= STROKE_STYLE_OFFSET ? l + STROKE_STYLE_OFFSET : l - STROKE_STYLE_OFFSET; - return `hsl(${h}, ${s}%, ${lValue}%)`; + public updateStyle(style: newPointStyle): void { + this.size = style.size ?? this.size; + this.fillStyle = style.fillStyle ?? this.fillStyle; + this.strokeStyle = style.strokeStyle ?? this.strokeStyle; + this.marker = style.marker ?? this.marker; + } + + public copy(): newPoint2D { + const copy = new newPoint2D(); + copy.center = this.center.copy(); + copy.size = this.size; + copy.marker = this.marker; + copy.markerOrientation = this.markerOrientation; + copy.fillStyle = this.fillStyle; + copy.strokeStyle = this.strokeStyle; + copy.lineWidth = this.lineWidth; + return copy } - public setColors(fillStyle: string = null, strokeStyle: string = null) { - this.fillStyle = fillStyle || this.fillStyle; - this.strokeStyle = strokeStyle; // ? strokeStyle : this.setStrokeStyle(this.fillStyle); + public update() { this.buildPath() } + + public setColors(color: string) { + this.fillStyle = this.isFilled ? color : null; + this.strokeStyle = this.isFilled ? this.setStrokeStyle(this.fillStyle) : color; } get drawnShape(): newShape { @@ -1884,6 +1954,7 @@ export class newPoint2D extends newShape { if (TRIANGLES.includes(this.marker)) marker = new Triangle(this.center.coordinates, this.size, this.markerOrientation); if (this.marker == 'halfLine') marker = new HalfLine(this.center.coordinates, this.size, this.markerOrientation); marker.lineWidth = this.lineWidth; + this.isFilled = marker.isFilled; return marker } @@ -1912,12 +1983,11 @@ export class newPoint2D extends newShape { public buildPath(): void { this.path = this.drawnShape.path }; - public setDrawingProperties(context: CanvasRenderingContext2D) { - context.lineWidth = this.lineWidth; - context.globalAlpha = this.alpha; - const fillColor = this.isHovered ? this.hoverStyle : this.isClicked ? this.clickedStyle : this.isSelected ? this.selectedStyle : this.fillStyle; - context.fillStyle = fillColor; - context.strokeStyle = this.strokeStyle ?? this.setStrokeStyle(fillColor); + public isInFrame(origin: Vertex, end: Vertex, scale: Vertex): boolean { + const inCanvasX = this.center.x * scale.x < end.x && this.center.x * scale.x > origin.x; + const inCanvasY = this.center.y * scale.y < end.y && this.center.y * scale.y > origin.y; + this.inFrame = inCanvasX && inCanvasY; + return this.inFrame } } @@ -1935,9 +2005,7 @@ export class ScatterPoint extends newPoint2D { ) { super(x, y, _size, _marker, _markerOrientation, fillStyle, strokeStyle); this.isScaled = false; - }; - - get tooltipMap(): Map { return this._tooltipMap }; + } public static fromPlottedValues(indices: number[], pointsData: {[key: string]: number[]}, pointSize: number, marker: string, thresholdDist: number, tooltipAttributes: string[], features: Map, axes: newAxis[], @@ -1951,22 +2019,22 @@ export class ScatterPoint extends newPoint2D { public updateTooltipMap() { this._tooltipMap = new Map([["Number", this.values.length], ["X mean", this.mean.x], ["Y mean", this.mean.y],]) }; - public update() { - this.isScaled = false; - this.buildPath(); - } - public updateTooltip(tooltipAttributes: string[], features: Map, axes: newAxis[], xName: string, yName: string) { this.updateTooltipMap(); if (this.values.length == 1) { this.newTooltipMap(); tooltipAttributes.forEach(attr => this.tooltipMap.set(attr, features.get(attr)[this.values[0]])); - } else { - this.tooltipMap.set(`Average ${xName}`, axes[0].isDiscrete ? axes[0].labels[Math.round(this.mean.x)] : this.mean.x); - this.tooltipMap.set(`Average ${yName}`, axes[1].isDiscrete ? axes[1].labels[Math.round(this.mean.y)] : this.mean.y); - this.tooltipMap.delete('X mean'); - this.tooltipMap.delete('Y mean'); + return; } + this.tooltipMap.set(`Average ${xName}`, axes[0].isDiscrete ? axes[0].labels[Math.round(this.mean.x)] : this.mean.x); + this.tooltipMap.set(`Average ${yName}`, axes[1].isDiscrete ? axes[1].labels[Math.round(this.mean.y)] : this.mean.y); + this.tooltipMap.delete('X mean'); + this.tooltipMap.delete('Y mean'); + } + + public updateStyle(style: newPointStyle): void { + super.updateStyle(style); + this.marker = this.values.length > 1 ? this.marker : style.marker ?? this.marker; } public computeValues(pointsData: {[key: string]: number[]}, thresholdDist: number): void { @@ -1986,12 +2054,63 @@ export class ScatterPoint extends newPoint2D { this.mean.x = meanX / this.values.length; this.mean.y = meanY / this.values.length; } +} - public isInFrame(xAxis: newAxis, yAxis: newAxis): boolean { - const inCanvasX = this.mean.x < xAxis.maxValue && this.mean.x > xAxis.minValue; - const inCanvasY = this.mean.y < yAxis.maxValue && this.mean.y > yAxis.minValue; - this.inFrame = inCanvasX && inCanvasY; - return this.inFrame +export class LineSequence extends newShape { + public previousTooltipOrigin: Vertex; + constructor( + public points: newPoint2D[] = [], + public name: string = "" + ) { + super(); + this.isScaled = false; + this.isFilled = false; + this.updateTooltipMap(); + } + + public initTooltip(context: CanvasRenderingContext2D): newTooltip { + const tooltip = super.initTooltip(context); + tooltip.isFlipper = true; + return tooltip + } + + public setTooltipOrigin(vertex: Vertex): void { + this.previousTooltipOrigin = vertex.copy(); + this.tooltipOrigin = this.previousTooltipOrigin.copy(); + } + + public translateTooltip(translation: Vertex): void { this.tooltipOrigin?.translateSelf(translation) } + + public mouseDown(mouseDown: Vertex) { this.setTooltipOrigin(mouseDown) } + + public updateTooltipMap() { this._tooltipMap = new Map([["Name", this.name]]) } + + private getEdgeStyle(edgeStyle: {[key: string]: any}): void { + if (edgeStyle.line_width) this.lineWidth = edgeStyle.line_width; + if (edgeStyle.color_stroke) this.strokeStyle = edgeStyle.color_stroke; + if (edgeStyle.dashline) this.dashLine = edgeStyle.dashline; + } + + public static getGraphProperties(graph: {[key: string]: any}): LineSequence { + const emptyLineSequence = new LineSequence([], graph.name); + if (graph.edge_style) emptyLineSequence.getEdgeStyle(graph.edge_style); + return emptyLineSequence + } + + public setDrawingProperties(context: CanvasRenderingContext2D) { + super.setDrawingProperties(context); + context.lineWidth = (this.isHovered || this.isClicked) ? this.lineWidth * 2 : this.lineWidth; + } + + public buildPath(): void { + this.path = new Path2D(); + this.path.moveTo(this.points[0].center.x, this.points[0].center.y); + this.points.slice(1).forEach(point=> this.path.lineTo(point.center.x, point.center.y)); + } + + public update(points: newPoint2D[]): void { + this.points = points; + this.buildPath(); } } @@ -2332,6 +2451,7 @@ export class newAxis extends EventEmitter { readonly SELECTION_RECT_SIZE = 10; readonly SIZE_END = 7; readonly FONT_SIZE = 12; + readonly isFilled = true; // OLD public is_drawing_rubberband: boolean = false; @@ -2411,7 +2531,6 @@ export class newAxis extends EventEmitter { } public resetScale(): void { - this.rubberBand.reset(); this.minValue = this.initMinValue; this.maxValue = this.initMaxValue; this._previousMin = this.initMinValue; @@ -2535,7 +2654,7 @@ export class newAxis extends EventEmitter { var [nameCoords, align, baseline, orientation] = this.topArrowTitleProperties(); } nameCoords.transformSelf(canvasHTMatrix); - const textParams: textParams = { + const textParams: TextParams = { width: this.drawLength, fontsize: this.FONT_SIZE, font: this.font, align: align, color: color, baseline: baseline, style: 'bold', orientation: orientation, backgroundColor: "hsla(0, 0%, 100%, 0.5)" }; @@ -2601,7 +2720,7 @@ export class newAxis extends EventEmitter { tickText.draw(context); } - private computeTickTextParams(): textParams { + private computeTickTextParams(): TextParams { const [textAlign, baseline] = this.textAlignments(); let textWidth = null; let textHeight = null; @@ -2627,7 +2746,7 @@ export class newAxis extends EventEmitter { return point } - private computeTickText(context: CanvasRenderingContext2D, text: string, tickTextParams: textParams, point: newPoint2D, HTMatrix: DOMMatrix): newText { + private computeTickText(context: CanvasRenderingContext2D, text: string, tickTextParams: TextParams, point: newPoint2D, HTMatrix: DOMMatrix): newText { const textOrigin = this.tickTextPositions(point, HTMatrix); const tickText = new newText(newText.capitalize(text), textOrigin, tickTextParams); tickText.removeEndZeros(); @@ -2765,21 +2884,27 @@ export class DrawingCollection { public updateMouseState(context: CanvasRenderingContext2D, mouseCoords: Vertex, stateName: string, keepState: boolean, invertState: boolean) { this.drawings.forEach(drawing => { - if (context.isPointInPath(drawing.path, mouseCoords.x, mouseCoords.y)) drawing[stateName] = invertState ? !drawing[stateName] : true - else { - if (!keepState) drawing[stateName] = false; + if (drawing.isFilled) { + if (context.isPointInPath(drawing.path, mouseCoords.x, mouseCoords.y)) drawing[stateName] = invertState ? !drawing[stateName] : true + else { + if (!keepState) drawing[stateName] = false; + } + } else { + context.save(); + context.lineWidth = 10; + if (context.isPointInStroke(drawing.path, mouseCoords.x, mouseCoords.y)) drawing[stateName] = invertState ? !drawing[stateName] : true + else { + if (!keepState) drawing[stateName] = false; + } + context.restore(); } }) } public mouseDown(mouseCoords: Vertex): any { let clickedObject: any = null; - this.drawings.forEach(drawing => { - if (drawing.isHovered) { - clickedObject = drawing; - clickedObject.mouseDown(mouseCoords); - } - }); + this.drawings.forEach(drawing => { if (drawing.isHovered) clickedObject = drawing }); + clickedObject?.mouseDown(mouseCoords); return clickedObject } } @@ -2805,7 +2930,7 @@ export class GroupCollection extends ShapeCollection { super(drawings, frame); } - public drawingIsContainer(drawing: any): boolean { return drawing.values?.length > 1 } + public drawingIsContainer(drawing: any): boolean { return drawing.values?.length > 1 || drawing instanceof LineSequence } public drawTooltips(canvasOrigin: Vertex, canvasSize: Vertex, context: CanvasRenderingContext2D, inMultiPlot: boolean): void { this.drawings.forEach(drawing => { if ((this.drawingIsContainer(drawing) || !inMultiPlot) && drawing.inFrame) drawing.drawTooltip(canvasOrigin, canvasSize, context) });