diff --git a/README.md b/README.md index e45df6e..b6c6613 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,34 @@ In short, this means that simply adding data points to a trace in `data` or chan ## API Reference +### usePlotly Hook + +As an alternative to the `Plot` component, you may use the `usePlotly` react _hook_. This provides a more powerful API with full control over the plot element, compatibility with functional components, intuitive responsive behaviour and ability to use `extendTraces`. +Here is a simple example of creating a chart with `usePlotly`: + +```jsx +function MyChart(props) { + const { ref, updates, appendData } = usePlotly(); + + // Here is a function that will change the data. You must pass a partial Figure object (plotly DSL object) which will be + // merged with all previous calls to `updates` + const changeData = () => updates({ data: [ { y: [Math.random() * 10], type: 'scatter' } ] }) + + // Here we start extending traces using the `appendData` stream + const extendData = setInterval(() => { + appendData({ data: { y: [[Math.random() * 10]]}, tracePos: [0] }); + }, 500); + + return ( +
+
+ + +
); +} +``` + + ### Basic Props **Warning**: for the time being, this component may _mutate_ its `layout` and `data` props in response to user input, going against React rules. This behaviour will change in the near future once https://github.com/plotly/plotly.js/issues/2389 is completed. diff --git a/package.json b/package.json index 945785a..5d0f853 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,9 @@ }, "peerDependencies": { "plotly.js": ">1.34.0", - "react": ">0.13.0" + "react": ">0.13.0", + "flyd": ">=0.2.8", + "ramda": ">=0.28.0" }, "browserify-global-shim": { "react": "React" diff --git a/src/usePlotly.js b/src/usePlotly.js new file mode 100644 index 0000000..b087988 --- /dev/null +++ b/src/usePlotly.js @@ -0,0 +1,50 @@ +import { useLayoutEffect, useState, useMemo } from 'react'; +import { head, prop, compose, pick, objOf, mergeDeepRight } from 'ramda'; +import { stream, scan } from 'flyd'; + +/** +* A simple debouncing function +*/ +const debounce = (fn, delay) => { + let timeout; + + return function (...args) { + const functionCall = () => fn.apply(this, args); + + timeout && clearTimeout(timeout); + timeout = setTimeout(functionCall, delay); + }; +}; + +const getSizeForLayout = compose(objOf('layout'), pick(['width', 'height']), prop('contentRect'), head); + +export default function usePlotly() { + const updates = useMemo(stream, []); + const appendData = useMemo(stream, []); + const plotlyState = useMemo( + () => scan(mergeDeepRight, { data: [], config: {}, layout: {} }, updates), + [] + ); + + const observer = new ResizeObserver(debounce(compose(updates, getSizeForLayout), 100)); + const [internalRef, setRef] = useState(null); + useLayoutEffect(() => { + if (internalRef) { + observer.observe(internalRef); + const endS = plotlyState.map(state => { + Plotly.react(internalRef, state); + }); + + const endAppend = appendData.map(({ data, tracePos }) => Plotly.extendTraces(internalRef, data, tracePos)); + + return () => { + Plotly.purge(internalRef); + observer.unobserve(internalRef); + endAppend.end(true); + endS.end(true); + }; + } + }, [internalRef, plotlyState, updates, appendData]); + + return { ref: setRef, updates, appendData }; +}