diff --git a/src/arraylayer.js b/src/arraylayer.js deleted file mode 100644 index 3c32a0b..0000000 --- a/src/arraylayer.js +++ /dev/null @@ -1,84 +0,0 @@ -import React from 'react'; // eslint-disable-line no-unused-vars -import Layer from './layer'; - -/** - * ArrayLayer wrapper for sigplot.layer1d and sigplot.layer2d - * - * This layer is meant for static 1D and 2D (or 1D with `framesize`) - * JS arrays/ArrayBuffers. A typical use case looks like - * - * For a 1-D spectral or time-series plot: - * - * - * - * - * - * For a 2-D raster/heatmap: - * - * - * - * - */ -class ArrayLayer extends Layer { - /** - * Handles ArrayLayer being mounted onto the DOM - * - * All we need to do when the component 'mounts', - * is call `plot.overlay_array` with the relevant - * data and options. This will return our layer object. - * - * A large portion of the time, especially for dynamic - * systems, this will look like - * `this.context.overlay_array([], undefined)` upon mount. - */ - componentDidMount() { - const { data, options, layerOptions } = this.props; - this.layer = this.context.overlay_array(data, options, layerOptions); - } - - /** - * Handles new properties being passed into - * - * UNSAFE_componentWillReceiveProps() replaced with - * shouldComponentUpdate() as they have similar calling patterns. - * We are using this method for a side-effect, and therefore - * returning True. getDerivedStateFromProps() had an additional - * call at mount which UNSAFE_componentWillReceiveProps() lacked. - * Thus the usage of shouldComponentUpdate(). - * - * This sits in the lifecycle right before `componentWillUpdate`, - * and most importantly `render`, so this is where we will call - * the plot's `reload` and `headermod` methods. - * - * @param nextProps the newly received properties - */ - shouldComponentUpdate(nextProps, _nextState) { - const { - data: currentData, - options: currentOptions, - layerOptions: currentLayerOptions, - } = this.props; - - const { - data: nextData, - options: nextOptions, - layerOptions: nextLayerOptions, - } = nextProps; - - // if the data changes, we'll go ahead - // and do a full `reload`; - // otherwise, we only need to headermod - // with the new options - if (nextData !== currentData) { - this.context.reload(this.layer, nextData, nextOptions); - } else if (nextOptions !== currentOptions) { - this.context.headermod(this.layer, nextOptions); - } else if (nextLayerOptions !== currentLayerOptions) { - this.context.get_layer(this.layer).change_settings(nextLayerOptions); - } - - return true; - } -} - -export default ArrayLayer; diff --git a/src/bluelayer.js b/src/bluelayer.js deleted file mode 100644 index 3e58896..0000000 --- a/src/bluelayer.js +++ /dev/null @@ -1,85 +0,0 @@ -import React from 'react'; // eslint-disable-line no-unused-vars -import Layer from './layer'; - -/** - * BlueLayer wrapper for sigplot.layer1d and sigplot.layer2d - * - * This layer is meant for Bluefiles - * ArrayBuffers. A typical use case looks like - * - * For a 1-D spectral or time-series plot: - * - * - * - * - * - * For a 2-D raster/heatmap: - * - * - * - * - */ -class BlueLayer extends Layer { - /** - * Handles BlueLayer being mounted onto the DOM - * - * All we need to do when the component 'mounts', - * is call `plot.overlay_bluefile` with the relevant - * data and options. This will return our layer object. - */ - componentDidMount() { - const { data, layerOptions } = this.props; - this.layer = this.context.overlay_bluefile(data, layerOptions); - } - - /** - * Handles new properties being passed into - * - * UNSAFE_componentWillReceiveProps() replaced with - * shouldComponentUpdate() as they have similar calling patterns. - * We are using this method for a side-effect, and therefore - * returning True. getDerivedStateFromProps() had an additional - * call at mount which UNSAFE_componentWillReceiveProps() lacked. - * Thus the usage of shouldComponentUpdate(). - * - * This sits in the lifecycle right before `componentWillUpdate`, - * and most importantly `render`, so this is where we will call - * the plot's `reload` and `headermod` methods. - * - * @param nextProps the newly received properties - */ - shouldComponentUpdate(nextProps, _nextState) { - const { - data: currentData, - options: currentOptions, - layerOptions: currentLayerOptions, - } = this.props; - - const { - data: nextData, - options: nextOptions, - layerOptions: nextLayerOptions, - } = nextProps; - - // if the data changes, we'll go ahead - // and do a full `reload`; - // otherwise, we only need to headermod - // with the new options - if (nextData !== currentData) { - this.context.reload(this.layer, nextData, nextOptions); - } else if (nextOptions !== currentOptions) { - this.context.headermod(this.layer, nextOptions); - } else if (nextLayerOptions !== currentLayerOptions) { - this.context.get_layer(this.layer).change_settings(nextLayerOptions); - } - - return true; - } -} - -export default BlueLayer; diff --git a/src/boxesplugin.tsx b/src/boxesplugin.tsx new file mode 100644 index 0000000..20c436e --- /dev/null +++ b/src/boxesplugin.tsx @@ -0,0 +1,200 @@ +import React, { useEffect, useState } from "react"; +import { Plot } from "sigplot"; +import Plugins from "../../node_modules/sigplot/js/plugins.js"; +import { box } from "./typing"; + +/** + * Boxes Plugin wrapper for sigplot + * + * This layer adds a plugin to sigplot + * + * + * + * + */ + +interface pluginOptions { + display?: boolean; + enableSelect?: boolean; + enableMove?: boolean; + enableResize?: boolean; + lineWidth?: number; + alpha?: number; + font?: string; + fill?: boolean; + strokeStyle?: string; + fillStyle?: string; + absolutePlacement?: boolean; +} + +interface pluginProps { + /** Options to be passed to the plugin regarding global defaults */ + options?: pluginOptions; + /** The reference to the Plot to which this plugin is attached. + * If this is a child component of SigPlot, then this gets added + * automatically for you and you should leave it blank. */ + plot?: Plot; + /** Any boxes passed to this property will be inserted onto the + * plot. If this input changes, all those items will be added to the + * plot without regard for if they were already there. In essence + * This just mimicks the boxAdd function in the plugin. I do allow + * passing a list of boxes in addition to one by one. + */ + addBox?: box[] | box; + /** Any box IDs passed to this function will be removed from the plot. + * It will "watch" changes much like addBox so you can interact with it. + * You can either pass a single box id or a list of box ids + */ + removeBox?: string[] | string; + /** If you would like to programatically take a box that is already + * on the screen and move it, then put the box info here. If the box.id + * contained in the box does not exist, then nothing will happen. + */ + moveBox?: box; + /** If this is set to True then a handler will be setup to add boxes + * to the plot on "mtag" events - i.e. if you hold control while drawing + * a box with your mouse. + */ + addOnMtag?: boolean; + /** If you move (or resize) a box on the plot, this callback will be + * triggered. The passed box will be the "new" box position. + */ + onMove?(box: box): void; + /** Subscribing to this callback will allow you to retrieve the IDs of + * any boxes added to the plot. + */ + onAdd?(box: box): void; + /** This callback will be triggered anytime a box is removed from the plot */ + onRemove?(box: box): void; + /** This callback will be triggered anytime a box on the plot is selected */ + onSelect?(box: box): void; +} + +function BoxesPlugin({ + plot, + options, + addBox, + removeBox, + moveBox, + addOnMtag, + onMove, + onAdd, + onRemove, + onSelect, +}: pluginProps) { + const [plugin, setPlugin] = useState(undefined); + + useEffect(() => { + // This will be called on mount + const bPlugin = new Plugins.BoxesPlugin(options); + plot.add_plugin(bPlugin, 2); + setPlugin(bPlugin); + + /** Add callbacks if the user wants them. Make sure that the + * despread operator is used everywhere as the objects being + * returned in event.box are just references to the object on + * the plot. That can cause weird behavior... Therefore, we + * will just return copies of those original objects. + */ + if (onMove) { + plot.addListener("boxmove", function (event) { + const curBox: box = { ...event.box }; + onMove(curBox); + }); + } + + if (onAdd) { + plot.addListener("boxadd", function (event) { + const curBox: box = { ...event.box }; + onAdd(curBox); + }); + } + + if (onSelect) { + plot.addListener("boxselect", function (event) { + const curBox: box = { ...event.box }; + onSelect(curBox); + }); + } + + if (onRemove) { + plot.addListener("boxremove", function (event) { + const curBox: box = { ...event.box }; + onRemove(curBox); + }); + } + + if (addOnMtag) { + plot.addListener("mtag", function (event) { + bPlugin.addBox({ x: event.x, y: event.y, h: event.h, w: event.w }); + }); + } + + /** There is a bug in the boxes plugin where if certain modes are selected + * then boxes with no width or height can be created. This is a hacky + * bugfix to tide over until the actual bug is fixed + * + * TODO Remove when original bug is fixed + */ + plot.addListener("boxadd", function (event) { + if (!event.box.w || !event.box.h) { + // There appears to be a bug in the boxes plugin where you can + // create boxes just by clicking on the screen of 0 w an 0 h + bPlugin.removeBox(event.box.id); + } + }); + + // Called on unmount + return () => { + plot.remove_plugin(bPlugin); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + if (plugin) { + if (addBox instanceof Array) { + addBox.forEach((box) => plugin.addBox(box)); + } else if (addBox) { + plugin.addBox(addBox); + } + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [plugin, addBox]); + + useEffect(() => { + if (plugin) { + if (removeBox instanceof Array) { + removeBox.forEach((box) => plugin.removeBox(box)); + } else if (removeBox) { + plugin.removeBox(removeBox); + } + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [plugin, removeBox]); + + useEffect(() => { + if (plugin) { + // Manually insert the box into the array held by the plugin + let curBoxes: box[] = plugin.getBoxes(); + const idx = curBoxes.findIndex((box) => box.id === moveBox.id); + if (idx !== -1) curBoxes[idx] = { ...moveBox }; + + // Redraw the plot + plot.redraw(); + + // Send out a resize event if we need to + if (onMove) { + onMove(moveBox); + } + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [plugin, moveBox]); + + return
; +} + +export default BoxesPlugin; diff --git a/src/datalayer.tsx b/src/datalayer.tsx new file mode 100644 index 0000000..897b3de --- /dev/null +++ b/src/datalayer.tsx @@ -0,0 +1,128 @@ +import React, { useEffect, useState } from "react"; +import { Plot } from "sigplot"; +import { dataOptions, layerOptions } from "./typing"; + +export interface dataLayerProps { + /** The data you intend to plot. This should be a numeric array if + * of type "array", "pipe", or "bluefile". It should be a string if + * of type "websocket" + */ + data: number[] | string; + + /** The format your data takes (bluefile, array, pipe, websocket) */ + format: "array" | "pipe" | "bluefile" | "websocket" | "wpipe"; + + /** Options for how to interpret the data as a bluefile */ + options?: dataOptions; + + /** Options for how to display the layer */ + layerOptions?: layerOptions; + + /** This is only used when doing a wpipe. It must be supplied if format === "wpipe". + * The fps stands for frames per second and sets the update rate. + */ + fps?: number; + + /** This in almost all instances should not be inserted by the user. This is intended + * to be a child of the in which case this is put in there automatically. + */ + plot?: Plot; +} + +function DataLayer({ + plot, + data, + format, + options, + layerOptions, + fps, +}: dataLayerProps) { + /** Other than hreflayer - this is the method used to get data to show up on the plot. + * Functionally, it allows setting the format to one of 5 different types. Each + * type is a different input type to sigplot. The different all call different + * sigplot functions under the hood, but there is enough commonality where it is + * easiest to keep them all together. Examples of how to call them are seen below: + * + * + * + * + * + * + * + * + * + */ + + const [layer, setLayer] = useState(undefined); + const [resetSocket, setResetSocket] = useState(false); + + useEffect(() => { + // Setup our layer & adjust if we change types. + let curLayer = undefined; + switch (format) { + case "array": + curLayer = plot.overlay_array(data, options, layerOptions); + break; + case "bluefile": + curLayer = plot.overlay_bluefile(data, options, layerOptions); + break; + case "websocket": + curLayer = plot.overlay_websocket(data, options, layerOptions); + break; + case "wpipe": + curLayer = plot.overlay_wpipe(data, options, layerOptions, fps); + break; + case "pipe": + curLayer = plot.overlay_pipe(data, options, layerOptions); + if (data !== undefined && data.length > 0) { + plot.push(curLayer, data); + } + break; + } + setLayer(curLayer); + + // This removes the layer on unmount or if the layer type changes. + return () => { + curLayer && plot.remove_layer(curLayer); + }; + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [format, resetSocket]); + + useEffect(() => { + // Push more data to the plot, or reload + if (layer) { + if (format === "pipe") { + plot.push(layer, data, options); + } else if (format === "websocket" || format === "wpipe") { + // We need to tear down the layer and make a new one. + // We created a state just for that. We don't want to remove and + // reset the layer here as it would break the teardown function. + setResetSocket(!resetSocket); + } else { + plot.reload(layer, data, options); + } + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [data, layer]); + + useEffect(() => { + // Change the data options + if (layer) { + plot.headermod(layer, options); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [options, layer]); + + useEffect(() => { + // Change the settings for the layer + if (layer) { + plot.get_layer(layer).change_settings(layerOptions); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [layerOptions, layer]); + + return
; +} + +export default DataLayer; diff --git a/src/hreflayer.js b/src/hreflayer.js deleted file mode 100644 index 066696c..0000000 --- a/src/hreflayer.js +++ /dev/null @@ -1,82 +0,0 @@ -import React from 'react'; // eslint-disable-line no-unused-vars -import PropTypes from 'prop-types'; -import Layer from './layer'; - -/** - * Wrapper around sigplot.Plot.overlay_href - * - * Typical use of this layer looks like - * - * - * - */ -class HrefLayer extends Layer { - static propTypes = { - /** - * URI to BLUEFILE or MATFILE to plot - * - * This can either be local 'file:///path/to/file' or - * remote 'http://myfile.com/file.tmp' - * - * Keep in mind that if the file is on a different domain, - * most browsers/web-servers will block cross origin requests. - * - * Since this layer doesn't take any numeric data, - * we are omitting the use of the `data` prop here. - */ - href: PropTypes.string, - - /** Callback that executes once the file loads */ - onload: PropTypes.func, - - /** Layer options */ - options: PropTypes.object, - }; - - static defaultProps = { - href: '', - onload: null, - }; - - /** - * On mount, all we need to do is call overlay_href - */ - componentDidMount() { - const { href, onload, options } = this.props; - this.layer = this.context.overlay_href(href, onload, options); - } - - /** - * Handles new properties being passed into - * - * UNSAFE_componentWillReceiveProps() replaced with - * shouldComponentUpdate() as they have similar calling patterns. - * We are using this method for a side-effect, and therefore - * returning True. getDerivedStateFromProps() had an additional - * call at mount which UNSAFE_componentWillReceiveProps() lacked. - * Thus the usage of shouldComponentUpdate(). - * - * This sits in the lifecycle right before `componentWillUpdate`, - * and most importantly `render`, so this is where we will call - * the plot's `reload` and `headermod` methods. - * - * @param nextProps the newly received properties - */ - shouldComponentUpdate(nextProps, _nextState) { - const { href: oldHref, options: oldOptions } = this.props; - - const { href: newHref, onload: newOnload, options: newOptions } = nextProps; - - // we only care if `href` or `options` changes - if (newHref !== oldHref) { - this.context.deoverlay(this.layer); - this.layer = this.context.overlay_href(newHref, newOnload, newOptions); - } else if (this.layer !== undefined && newOptions !== oldOptions) { - this.context.get_layer(this.layer).change_settings(newOptions); - } - - return true; - } -} - -export default HrefLayer; diff --git a/src/hreflayer.tsx b/src/hreflayer.tsx new file mode 100644 index 0000000..71de07b --- /dev/null +++ b/src/hreflayer.tsx @@ -0,0 +1,88 @@ +import React, { useEffect, useState } from "react"; +import PropTypes from "prop-types"; +import { Plot } from "sigplot"; +import { layerOptions } from "./typing"; + +/** + * Wrapper around sigplot.Plot.overlay_href + * + * Typical use of this layer looks like + * + * + * + */ + +const propTypes = { + /** + * URI to BLUEFILE or MATFILE to plot + * + * This can either be local 'file:///path/to/file' or + * remote 'http://myfile.com/file.tmp' + * + * Keep in mind that if the file is on a different domain, + * most browsers/web-servers will block cross origin requests. + * + * Since this layer doesn't take any numeric data, + * we are omitting the use of the `data` prop here. + */ + href: PropTypes.string, + + /** Callback that executes once the file loads */ + onload: PropTypes.func, + + /** Layer options */ + options: PropTypes.object, +}; + +const defaultProps = { + href: "", + onload: null, +}; + +interface HrefProps { + /** href or |-delimited hrefs the url to the bluefile or matfile */ + href?: string; + onload?: CallableFunction; + layerOptions?: layerOptions; + plot?: Plot; +} + +function HrefLayer({ plot, href, onload, layerOptions }: HrefProps) { + const [layer, setLayer] = useState(undefined); + + useEffect(() => { + // Called on Mount + const curLayer = plot.overlay_href(href, onload, layerOptions); + setLayer(curLayer); + + // This will be called on unmount + return () => { + plot.remove_layer(curLayer); + }; + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + if (layer) { + plot.deoverlay(layer); + setLayer(plot.overlay_href(href, onload, layerOptions)); + } + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [href]); + + useEffect(() => { + if (layer) { + plot.get_layer(layer).change_settings(layerOptions); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [layerOptions]); + + return
; +} + +HrefLayer.propTypes = propTypes; +HrefLayer.defaultProps = defaultProps; + +export default HrefLayer; diff --git a/src/index.js b/src/index.js deleted file mode 100644 index 1b0e7ad..0000000 --- a/src/index.js +++ /dev/null @@ -1,8 +0,0 @@ -export { default as SigPlot } from './sigplot'; -export { default as ArrayLayer } from './arraylayer'; -export { default as PipeLayer } from './pipelayer'; -export { default as HrefLayer } from './hreflayer'; -export { default as BlueLayer } from './bluelayer'; -export { default as WebsocketLayer } from './websocketlayer'; -export { default as Layer } from './layer'; -export { default as Plugin } from './plugin'; diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..e8723c2 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,4 @@ +export { default as SigPlot } from './sigplot'; +export { default as DataLayer } from './datalayer'; +export { default as HrefLayer } from './hreflayer'; +export { default as BoxesPlugin } from './boxesplugin'; diff --git a/src/layer.js b/src/layer.js deleted file mode 100644 index f8bea4e..0000000 --- a/src/layer.js +++ /dev/null @@ -1,45 +0,0 @@ -import React, { Component } from 'react'; // eslint-disable-line no-unused-vars -import PropTypes from 'prop-types'; -import { PlotContext } from './sigplot'; - -/** - * Abstract base class for all Layers - */ -class Layer extends Component { - static propTypes = { - /** Array of `Number` types */ - data: PropTypes.arrayOf(PropTypes.number), // eslint-disable-line react/no-unused-prop-types - - /** Header options for `data` */ - options: PropTypes.object, // eslint-disable-line react/no-unused-prop-types - - /** - * Options about the layer - * - * @see See [sigplot.layer1d](https://github.com/LGSInnovations/sigplot/blob/master/js/sigplot.layer1d.js) - * @see See [sigplot.layer2d](https://github.com/LGSInnovations/sigplot/blob/master/js/sigplot.layer2d.js) - */ - layerOptions: PropTypes.object, // eslint-disable-line react/no-unused-prop-types - }; - - static contextType = PlotContext; - - /** - * On unmount, all we need to do is remove the layer - * from the plot. - */ - componentWillUnmount() { - this.context.remove_layer(this.layer); - } - - /** - * The layer components don't _actually_ render to the DOM. - * - * They are merely abstractions of canvas-manipulations. - */ - render() { - return false; - } -} - -export default Layer; diff --git a/src/pipelayer.js b/src/pipelayer.js deleted file mode 100644 index 9c84073..0000000 --- a/src/pipelayer.js +++ /dev/null @@ -1,81 +0,0 @@ -import React from 'react'; // eslint-disable-line no-unused-vars -import Layer from './layer'; - -/** - * Wrapper around sigplot.Plot.overlay_pipe - * - * This wrapper is for streaming 1-D plots or - * 2-D raster waterfall plots. - * - * Typical use of this would look like - * - * - * - * - * - * where options is populated before data begins flowing - */ -class PipeLayer extends Layer { - /** - * On mount, the only action we can take is to overlay the - * pipe with the specified header (`options`) information - * - * It isn't until data begins coming that we can begin to - */ - componentDidMount() { - const { options, data, layerOptions } = this.props; - - // start by setting the header of the pipe - this.layer = this.context.overlay_pipe(options, layerOptions); - - // if data is provided and non-empty, go ahead and - // begin plotting data - if (data !== undefined && data.length > 0) { - this.context.push(this.layer, data); - } - } - - /** - * Handles new properties being passed into - * - * UNSAFE_componentWillReceiveProps() replaced with - * shouldComponentUpdate() as they have similar calling patterns. - * We are using this method for a side-effect, and therefore - * returning True. getDerivedStateFromProps() had an additional - * call at mount which UNSAFE_componentWillReceiveProps() lacked. - * Thus the usage of shouldComponentUpdate(). - * - * This sits in the lifecycle right before `componentWillUpdate`, - * and most importantly `render`, so this is where we will call - * the plot's `reload` and `headermod` methods. - * - * @param nextProps the newly received properties - * - * @TODO Handle headermod updates - */ - shouldComponentUpdate(nextProps, _nextState) { - const { - data: currentData, - options: currentOptions, - layerOptions: currentLayerOptions, - } = this.props; - const { - data: nextData, - options: nextOptions, - layerOptions: nextLayerOptions, - } = nextProps; - - // if new data has come in, plot that - if (nextData && nextData !== currentData) { - this.context.push(this.layer, nextData, nextOptions); - } else if (nextOptions !== currentOptions) { - this.context.headermod(this.layer, nextOptions); - } else if (nextLayerOptions !== currentLayerOptions) { - this.context.get_layer(this.layer).change_settings(nextLayerOptions); - } - - return true; - } -} - -export default PipeLayer; diff --git a/src/plugin.js b/src/plugin.js deleted file mode 100644 index 54abe82..0000000 --- a/src/plugin.js +++ /dev/null @@ -1,50 +0,0 @@ -import React, { Component } from 'react'; // eslint-disable-line no-unused-vars -import PropTypes from 'prop-types'; -import { PlotContext } from './sigplot'; - -/** - * Abstract base class for all Plugins - */ -class Plugin extends Component { - static propTypes = { - /** - * Options about the plugin - * - * @see See [plugins](https://github.com/LGSInnovations/sigplot/blob/master/js/plugins.js) - */ - pluginOptions: PropTypes.object, // eslint-disable-line react/no-unused-prop-types - }; - - static contextType = PlotContext; - - /** - * On unmount, all we need to do is remove the plugin - * from the plot. - */ - componentWillUnmount() { - this.context.remove_plugin(this.plugin); - } - - /** - * Getter for the sigplot.Plot object - * - * The `plot` is 'given' to the plugin-children - * from the parent component, so we receive - * it from the context. - */ - get plot() { - const { plot } = this.context; - return plot; - } - - /** - * The plugin components don't _actually_ render to the DOM. - * - * They are merely abstractions of canvas-manipulations. - */ - render() { - return false; - } -} - -export default Plugin; diff --git a/src/sigplot.js b/src/sigplot.js deleted file mode 100644 index 660940c..0000000 --- a/src/sigplot.js +++ /dev/null @@ -1,148 +0,0 @@ -import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -import { Plot } from 'sigplot'; - -export const PlotContext = React.createContext(undefined); - -/** - * SigPlot.js React wrapper class - * - * @version 0.1.15 - * @visibleName SigPlot.js React Wrapper - */ -class SigPlot extends Component { - static propTypes = { - /** - * Different Layer nodes (e.g., ArrayLayer, PipeLayer, etc.) - * - * @ignore - */ - children: PropTypes.node, - - /** Height of the wrapper div */ - height: PropTypes.number, - - /** Width of the wrapper div */ - width: PropTypes.number, - - /** CSS 'display' property */ - display: PropTypes.string, - - /** Styles object for any other CSS styles on the wrapper div */ - styles: PropTypes.object, - - /** - * SigPlot plot-level options - * - * @see See [sigplot.Plot Docs](http://sigplot.lgsinnovations.com/html/doc/sigplot.Plot.html) - */ - options: PropTypes.object, - }; - - static defaultProps = { - height: 300, - width: 300, - display: 'inline-block', - options: { - all: true, - expand: true, - autol: 100, - autohide_panbars: true, - }, - }; - - constructor(props) { - super(props); - this.state = {}; - } - - componentDidMount() { - const { options } = this.props; - this.plot = new Plot(this.element, options); - - // Have to trigger context tree, setting state does that. - // eslint-disable-next-line react/no-did-mount-set-state - // eslint-disable-next-line react/no-unused-state - this.setState({ plot: this.plot }); - } - - shouldComponentUpdate(nextProps, _nextState) { - const { height, width, options } = this.props; - const { - height: newHeight, - width: newWidth, - options: newOptions, - } = nextProps; - - // When the outer div height/width changes, - // we need to explicitly tell SigPlot to resize; - // otherwise, it won't resize automatically. - if (newHeight !== height || newWidth !== width) { - this.plot.checkresize(); - } - - // If the options change at the plot level, - // we need to handle that - if (newOptions !== options) { - this.plot.change_settings(newOptions); - } - - return true; - } - - render() { - const { - height, - width, - display, - styles, - children: propChildren, - } = this.props; - const { plot } = this; - - /** - * Recall we're treating the `sigplot.layer1d` and - * `sigplot.layer2d` as (virtual) children nodes since - * they are simply manipulations/API calls that modify - * the underlying . - * - * As such, the user should never have to access the - * `children` property outright, instead being able to - * write - * - * - * - * - * - * Anyway, the point of the following statement is - * to provide the `plot` object (controlled by the parent) - * to the child so it can mutate it. - */ - const children = plot - ? React.Children.map(propChildren, (child) => { - if (child) { - return React.cloneElement(child, { plot }); - } - return null; - }) - : null; - - return ( - -
(this.element = element)} - > - {children} -
-
- ); - } -} - -export default SigPlot; diff --git a/src/sigplot.tsx b/src/sigplot.tsx new file mode 100644 index 0000000..88ad815 --- /dev/null +++ b/src/sigplot.tsx @@ -0,0 +1,114 @@ +import React, { useEffect, useLayoutEffect, useRef, useState } from "react"; +import { Plot } from "sigplot"; +import { sigplotOptions } from "./typing"; + +interface sigplotProps { + /** Height of the wrapper div */ + height?: number; + /** Width of the wrapper div */ + width?: number; + /** Callback for `mtag` events which are generated with + * clicking and dragging to create a box while holding ctrl. + * Returns just the information about the box. + */ + onMtag?(event: mtagEvent): void; + /** Styles object for any other CSS styles on the wrapper div */ + style?: React.CSSProperties; + /** + * SigPlot plot-level options + * + * @see See [sigplot.Plot Docs](http://sigplot.lgsinnovations.com/html/doc/sigplot.Plot.html) + */ + options?: sigplotOptions; + /** Any plugins or layers that should be added to the plot */ + children?: any; +} + +export interface mtagEvent { + x: number; + y: number; + w: number; + h: number; + xpos: number; + ypos: number; + wpxl: number; + hpxl: number; +} + +const defaultProps = { + height: 300, + width: 300, + style: { display: "inline-block" }, + options: { + all: true, + expand: true, + autol: 100, + autohide_panbars: true, + }, +}; + +function SigPlot({ + children, + height, + width, + style, + options, + onMtag, +}: sigplotProps) { + const [plot, setPlot] = useState(undefined); + const plotRef = useRef(); + + // This only runs when it is mounted due to the empty array + useEffect(() => { + const plot = new Plot(plotRef.current, options); + if (onMtag) { + plot.addListener("mtag", (event: any) => { + const eventData: mtagEvent = { + x: event.x, + y: event.y, + h: event.h, + w: event.w, + xpos: event.xpos, + ypos: event.ypos, + wpxl: event.wpxl, + hpxl: event.hpxl, + }; + onMtag(eventData); + }); + } + setPlot(plot); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + // Anytime the width or height changes, this will kick off + useLayoutEffect(() => plot && plot.checkresize(), [plot, width, height]); + + // Anytime the options change this will fire + useEffect(() => plot && plot.change_settings(options), [plot, options]); + + const plotChildren = plot + ? React.Children.map(children, (child) => { + if (child) { + return React.cloneElement(child, { plot }); + } + return null; + }) + : null; + + return ( +
+ {plotChildren} +
+ ); +} + +SigPlot.defaultProps = defaultProps; + +export default SigPlot; diff --git a/src/typing.ts b/src/typing.ts new file mode 100644 index 0000000..8b2e355 --- /dev/null +++ b/src/typing.ts @@ -0,0 +1,208 @@ +type inCmode = "IN__MA" | "IN__Magnitude" | "IN__PH" | "IN__Phase" | "IN__RE" | "IN__Real" | "IN__IM" | + "IN__Imaginary" | "IN__LO" | "IN__D1" | "IN__10log10" | "IN__L2" | "IN__D2" | + "IN__20log20" | "IN__RI" | "IN__Real/Imag" | "IN__Image/Real" | "IN__IR"; +type abCmode = "AB__MA" | "AB__Magnitude" | "AB__PH" | "AB__Phase" | "AB__RE" | "AB__Real" | "AB__IM" | + "AB__Imaginary" | "AB__LO" | "AB__D1" | "AB__10log10" | "AB__L2" | "AB__D2" | + "AB__20log20" | "AB__RI" | "AB__Real/Imag" | "AB__Image/Real" | "AB__IR"; +type cmode = "IN" | "AB" | "MA" | "Magnitude" | "PH" | "Phase" | "RE" | "Real" | "IM" | + "Imaginary" | "LO" | "D1" | "10log10" | "L2" | "D2" | "20log20" | "RI" | + "Real/Imag" | "Image/Real" | "IR" | inCmode | abCmode; + +interface colors { + fg?: string; + bg?: string; +} + +export interface sigplotOptions { + cmode?: cmode; + phunits?: "D" | "R" | "C"; + cross?: boolean; + nogrid?: boolean; + legend?: boolean; + no_legend_button?: boolean; + nopan?: boolean; + nomenu?: boolean; + nospec?: boolean; + noxaxis?: boolean; + noyaxis?: boolean; + noreadout?: boolean; + nodragdrop?: boolean; + scroll_time_interval?: number; + index?: boolean; + autox?: 0 | 1 | 2 | 3; + xmin?: number; + xmax?: number; + xlab?: units; + xlabel?: object; + xdiv?: number; + xcnt?: 0|1|2; + rubberbox_mode?: "zoom" | "box"; + rightclick_rubberbox_mode?: "zoom" | "box"; + line?: 0 | 1 | 2 | 3; + autoy?: 0 | 1 | 2 | 3; + ylab?: units; + ylabel?: object; + ymin?: number; + ymax?: number; + ydiv?: number; + zmin?: number; + zmax?: number; + yinv?: boolean; + colors?: colors; + xi?: boolean; + all?: boolean; + expand?: boolean; + origin?: 1 | 2 | 3 | 4; + bufmax?: number; + nokeypress?: boolean; + font_family?: string; + font_scaled?: boolean; + font_width?: number; +} + +export interface layerOptions { + /** These options override ways that the layer of the plot are + * displayed to the user + */ + + /** Override the x-midas data-type */ + layerType?: "1D" | "2D" | "1DSDS" | "2DSDS"; + /** This expands the plot range to accomodate if this isn't the first layer */ + expand?: boolean; + /** Any data that you want to store in the layer */ + user_data?: any; + /** Set the framesize for the plot */ + framesize?: number; + /** Set the display name for the layer */ + name?: string; + } + +export interface dataOptions { + /** These options override whatever key/value pairs you desire in the + * header control block of the bluefile - even if you are just passing + * in an array of data, or reading from a non-xmidas pipe, internally + * a header control block is created + */ + + /** The type of the xmidas file being plotted */ + type?: 1000 | 2000; + + /** The size of data to be plotted - if set, sigplot forces type 2000 */ + subsize?: number; + + /** The format of the data (CF/SF/etc) */ + format?: string; + + /** The start time for the file in J1950 format */ + timecode?: number; + + xstart?: number; + + /** The change in `units` between subsequent points */ + xdelta?: number; + + /** The units for the x-axis */ + xunits?: units; + + ystart?: number; + + /** The change in `units` between subsequent points */ + ydelta?: number; + + /** The units for the y-axis */ + yunits?: units; + + /** pipe size for piped data. */ + pipesize?: number; + } + +/** units Structure: + * 0: ["None", "U"], + * 1: ["Time", "sec"], + * 2: ["Delay", "sec"], + * 3: ["Frequency", "Hz"], + * 4: ["Time code format", ""], + * 5: ["Distance", "m"], + * 6: ["Speed", "m/s"], + * 7: ["Acceleration", "m/sec^2"], + * 8: ["Jerk", "m/sec^3"], + * 9: ["Doppler", "Hz"], + * 10: ["Doppler rate", "Hz/sec"], + * 11: ["Energy", "J"], + * 12: ["Power", "W"], + * 13: ["Mass", "g"], + * 14: ["Volume", "l"], + * 15: ["Angular power density", "W/ster"], + * 16: ["Integrated power density", "W/rad"], + * 17: ["Spatial power density", "W/m^2"], + * 18: ["Integrated power density", "W/m"], + * 19: ["Spectral power density", "W/MHz"], + * 20: ["Amplitude", "U"], + * 21: ["Real", "U"], + * 22: ["Imaginary", "U"], + * 23: ["Phase", "rad"], + * 24: ["Phase", "deg"], + * 25: ["Phase", "cycles"], + * 26: ["10*Log", "U"], + * 27: ["20*Log", "U"], + * 28: ["Magnitude", "U"], + * 29: ["Unknown", "U"], + * 30: ["Unknown", "U"], + * 31: ["General dimensionless", ""], + * 32: ["Counts", ""], + * 33: ["Angle", "rad"], + * 34: ["Angle", "deg"], + * 35: ["Relative power", "dB"], + * 36: ["Relative power", "dBm"], + * 37: ["Relative power", "dBW"], + * 38: ["Solid angle", "ster"], + * 40: ["Distance", "ft"], + * 41: ["Distance", "nmi"], + * 42: ["Speed", "ft/sec"], + * 43: ["Speed", "nmi/sec"], + * 44: ["Speed", "knots=nmi/hr"], + * 45: ["Acceleration", "ft/sec^2"], + * 46: ["Acceleration", "nmi/sec^2"], + * 47: ["Acceleration", "knots/sec"], + * 48: ["Acceleration", "G"], + * 49: ["Jerk", "G/sec"], + * 50: ["Rotation", "rps"], + * 51: ["Rotation", "rpm"], + * 52: ["Angular velocity", "rad/sec"], + * 53: ["Angular velocity", "deg/sec"], + * 54: ["Angular acceleration", "rad/sec^2"], + * 55: ["Angular acceleration", "deg/sec^2"], + * 60: ["Latitude", "deg"], + * 61: ["Longitude", "deg"], + * 62: ["Altitude", "ft"], + * 63: ["Altitude", "m"] + */ +export type units = 0|1|2|3|4|5|6|7|8|9|10|11|12|13|14|15|16|17|18|19| + 20|21|22|23|24|25|26|27|28|29|30|31|32|33|34|35|36|37|38| + 40|41|42|43|45|46|47|48|49|50|51|52|53|54|55|60|61|62|63; + +export interface box { + /** The X coordinate of the box */ + x: number; + /** The Y coordinate of the box */ + y: number; + /** The width of the box */ + w: number; + /** The height of the box */ + h: number; + /** An optional label for the box */ + text?: string; + /** Do you want the box filled in? */ + fill?: boolean; + /** What style would you like your fill? */ + fillStyle?: string; + /** What should the transparency value be on fill/select */ + alpha?: number; + /** How thick do you want the box border */ + lineWidth?: number; + /** Do you want to use the pixel coordinate space? */ + absolutePlacement?: boolean; + /** This value is returned by the plugin when a box is created. + * It will be overriden if passed when creating a box */ + id?: string; +} \ No newline at end of file diff --git a/src/websocketlayer.js b/src/websocketlayer.js deleted file mode 100644 index 1c0535b..0000000 --- a/src/websocketlayer.js +++ /dev/null @@ -1,90 +0,0 @@ -import React from 'react'; // eslint-disable-line no-unused-vars -import PropTypes from 'prop-types'; -import Layer from './layer'; - -/** - * Wrapper around sigplot.Plot.overlay_websocket - * - * Typical use of this layer looks like - * - * - * - */ -class WebsocketLayer extends Layer { - static propTypes = { - /** - * URI to websocket server - * - * This usually looks like ws://: - * - * Keep in mind that if the websocket server is on a different domain, - * most browsers/web-servers will block cross origin requests. - * - * Since this layer doesn't take any numeric data, - * we are omitting the use of the `data` prop here. - */ - wsurl: PropTypes.string, - - /** Key-value pairs whose values alter plot settings */ - overrides: PropTypes.object, - - /** Layer options */ - options: PropTypes.object, - }; - - static defaultProps = { - wsurl: '', - }; - - /** - * On mount, all we need to do is call overlay_websocket - */ - componentDidMount() { - const { wsurl, overrides, options } = this.props; - this.layer = this.context.overlay_websocket(wsurl, overrides, options); - } - - /** - * Handles new properties being passed into - * - * UNSAFE_componentWillReceiveProps() replaced with - * shouldComponentUpdate() as they have similar calling patterns. - * We are using this method for a side-effect, and therefore - * returning True. getDerivedStateFromProps() had an additional - * call at mount which UNSAFE_componentWillReceiveProps() lacked. - * Thus the usage of shouldComponentUpdate(). - * - * This sits in the lifecycle right before `componentWillUpdate`, - * and most importantly `render`, so this is where we will call - * the plot's `reload` and `headermod` methods. - * - * @param nextProps the newly received properties - * - * @TODO Investigate whether deoverlay is necessary here - */ - shouldComponentUpdate(nextProps, _nextState) { - const { wsurl: oldWsurl, options: oldOptions } = this.props; - - const { - wsurl: newWsurl, - overrides: newOverrides, - options: newOptions, - } = nextProps; - - // we only care if `wsurl` or `options` changes; - if (newWsurl !== oldWsurl) { - this.context.deoverlay(this.layer); - this.layer = this.context.overlay_websocket( - newWsurl, - newOverrides, - newOptions - ); - } else if (this.layer !== undefined && newOptions !== oldOptions) { - this.context.get_layer(this.layer).change_settings(newOptions); - } - - return true; - } -} - -export default WebsocketLayer; diff --git a/src/wpipelayer.js b/src/wpipelayer.js deleted file mode 100644 index efa993a..0000000 --- a/src/wpipelayer.js +++ /dev/null @@ -1,98 +0,0 @@ -import React from 'react'; // eslint-disable-line no-unused-vars -import PropTypes from 'prop-types'; -import Layer from './layer'; - -class WPipeLayer extends Layer { - static propTypes = { - /** - * URI to WPIPE websocket server - * - * This usually looks like ws://: - * - * Keep in mind that if the websocket server is on a different domain, - * most browsers/web-servers will block cross origin requests. - * - * Since this layer doesn't take any numeric data, - * we are omitting the use of the `data` prop here. - */ - wsurl: PropTypes.string, - - /** Key-value pairs whose values alter plot settings */ - overrides: PropTypes.object, - - /** Layer options */ - options: PropTypes.object, - - /** Frames per second throttles the data flow to the client by the specified */ - fps: PropTypes.number, - }; - - /** - * Handles WPipeLayer being mounted onto the DOM - * - * All we need to do when the component 'mounts', - * is call `plot.overlay_wpipe` with the relevant - * websocket url and options. This will return our layer object. - * - * A large portion of the time, especially for dynamic - * systems, this will look like a single - * `this.context.overlay_wpipe(wsurl, null, {"layerType": "1D", pipesize: ...)` - * upon mount. - */ - componentDidMount() { - const { wsurl, options, layerOptions, fps } = this.props; - this.layer = this.context.overlay_wpipe(wsurl, options, layerOptions, fps); - } - - /** - * Handles new properties being passed into - * - * UNSAFE_componentWillReceiveProps() replaced with - * shouldComponentUpdate() as they have similar calling patterns. - * We are using this method for a side-effect, and therefore - * returning True. getDerivedStateFromProps() had an additional - * call at mount which UNSAFE_componentWillReceiveProps() lacked. - * Thus the usage of shouldComponentUpdate(). - * - * This sits in the lifecycle right before `componentWillUpdate`, - * and most importantly `render`, so this is where we will call - * the plot's `reload` and `headermod` methods. - * - * @param nextProps the newly received properties - */ - shouldComponentUpdate(nextProps, _nextState) { - const { - wsurl: currentWsurl, - options: currentOptions, - layerOptions: currentLayerOptions, - fps: currentFps, - } = this.props; - const { - wsurl: nextWsurl, - options: nextOptions, - layerOptions: nextLayerOptions, - fps: nextFps, - } = nextProps; - - // if the wsurl changes, we'll go ahead - // and delete the layer and create a new one - // otherwise, we only need to headermod - // with the new options - if (nextWsurl !== currentWsurl || currentFps !== nextFps) { - this.context.delete_layer(this.layer); - this.layer = this.context.overlay_wpipe( - nextWsurl, - nextOptions, - nextLayerOptions, - nextFps - ); - } else if (nextOptions !== currentOptions) { - this.context.headermod(this.layer, nextOptions); - } else if (nextLayerOptions !== currentLayerOptions) { - this.context.get_layer(this.layer).change_settings(nextLayerOptions); - } - return true; - } -} - -export default WPipeLayer;