I'm experimenting with drag-and-drop using cyclejs in a codepen. The standard drag methods supported by HTML 5 don't seem to support constraints on the movement of the dragged object so I went with standard mousedown/mousemove/mouseup. It works, but not consistently. The combine() operation doesn't seem to trigger even when the debug() calls show that mousedown and mousemove events have been received and sometimes the mouseup is missed. Perhaps my understanding of the operation is incomplete or incorrect. A direct link to the codepen is provided at the bottom of this post. Any help appreciated!
const xs = xstream.default;
const { run } = Cycle;
const { div, svg, makeDOMDriver } = CycleDOM;
function DragBox(sources) {
const COMPONENT_NAME = `DragBox`;
const intent = function({ DOM }) {
return {
mousedown$: DOM.select(`#${COMPONENT_NAME}`)
.events("mousedown")
.map(function(ev) {
return ev;
})
.debug("mousedown"),
mousemove$: DOM.select(`#${COMPONENT_NAME}`)
.events("mousemove")
.map(function(ev) {
return ev;
})
.debug("mousemove"),
mouseup$: DOM.select("#container")
.events("mouseup")
.map(function(ev) {
return ev;
})
.debug("mouseup")
};
};
const between = (first, second) => {
return source => first.mapTo(source.endWhen(second)).flatten();
};
const model = function({ mousedown$, mousemove$, mouseup$ }) {
return xs
.combine(mousedown$, mousemove$)
.debug("combine")
.map(([mousedown, mousemove]) => ({
x: mousemove.pageX - mousedown.layerX,
y: mousemove.pageY - mousedown.layerY
}))
.compose(between(mousedown$, mouseup$))
.startWith({
x: 0,
y: 0
})
.debug("model");
};
const getStyle = left => top => {
return {
style: {
position: "absolute",
left: left + "px",
top: top + "px",
backgroundColor: "#333",
cursor: "move"
}
};
};
const view = function(state$) {
return state$.map(value =>
div("#container", { style: { height: "100vh" } }, [
div(`#${COMPONENT_NAME}`, getStyle(value.x)(value.y), "Move Me!")
])
);
};
const actions = intent(sources);
const state$ = model(actions);
const vTree$ = view(state$);
return {
DOM: vTree$
};
}
function main(sources) {
const dragBox = DragBox(sources);
const sinks = {
DOM: dragBox.DOM
};
return sinks;
}
Cycle.run(main, {
DOM: makeDOMDriver("#app")
});
https://codepen.io/velociflapter/pen/bvqMGp?editors=1111
Testing your code shows that combine is not getting the first mousedown event, apparently due to the between operator subscribing to mousedown$ after the first mousedown event. Adding remember to the mousedown$ sends that first mousedown event to the between operator subscription.
mousedown$: DOM.select(`#${COMPONENT_NAME}`)
.events("mousedown").remember()
Codepen.io remember example.
Codesandbox.io testing between
Here's another CycleJS/xstream Drag and Drop approach (taking inspiration from this RxJS Drag and Drop example) I think is more straight forward. Everything else in your code is essentially the same but the model function is this:
const model = function({ mousedown$, mousemove$, mouseup$ }) {
return mousedown$.map(md => {
let startX = md.offsetX, startY = md.offsetY
return mousemove$.map(mm => {
mm.preventDefault()
return {
x: mm.clientX - startX,
y: mm.clientY - startY
}
}).endWhen(mouseup$)
}).flatten().startWith({x:0,y:0})
};
Here's a Codepen.io example.
Related
I have the logic of disposing of lines in my chart and have a custom cursor that I get from this link and when I dispose the line the label shouldn't show too and it works, but after restoring the rowY the names in textbox is pale, look at the next screenshots pale labels image, normal labels before disposing
rowsY.map((rowY, i) => {
this.seriesInstances[i][1].isDisposed() ? rowY.dispose() : rowY.restore();
if (nearestDataPoints[i]?.location?.y) {
rowY.setText(`${this.seriesInstances[i][1].getName()}: ${+this.chartInstance.getDefaultAxisY().formatValue(nearestDataPoints[i].location.y)} ${this.seriesInitialData[i].unit}`)
}
});
It seems you dispose/restore rowY in onSeriesBackgroundMouseMove.
I'd suggest to do that in the place where you dispose/restore the seriesInstances but still it will not work with legend box.
To fix it you can just dispose RowsY and RowX instead of resultTable and restore it after check:
rowX.restore()
series.forEach((el, i)=>{
//check if series was disposed
if(!el.isDisposed()){
rowsY[i].restore()
}
})
Also, in v.4.0 we will add new API that replaces dispose/restore
Here is updated code from the example that you use
// Import LightningChartJS
const lcjs = require("#arction/lcjs");
// Import data-generators from 'xydata'-library.
const { createProgressiveTraceGenerator } = require("#arction/xydata");
// Extract required parts from LightningChartJS.
const {
lightningChart,
AutoCursorModes,
UIElementBuilders,
UILayoutBuilders,
UIOrigins,
translatePoint,
Themes,
} = lcjs;
// Create a XY Chart.
const chart = lightningChart()
.ChartXY({
theme: Themes.lightNew,
})
// Disable native AutoCursor to create custom
.setAutoCursorMode(AutoCursorModes.disabled)
.setTitle('Custom Cursor using LCJS UI')
// set title for Y axis
chart.getDefaultAxisY().setTitle('Y-axis')
// generate data and creating the series
const series = new Array(3).fill(0).map((_, iSeries) => {
const nSeries = chart.addLineSeries({
dataPattern: {
// pattern: 'ProgressiveX' => Each consecutive data point has increased X coordinate.
pattern: 'ProgressiveX',
},
})
createProgressiveTraceGenerator()
.setNumberOfPoints(200)
.generate()
.toPromise()
.then((data) => {
return nSeries.add(data)
})
return nSeries
})
// Add Legend.
const legend = chart.addLegendBox().add(chart)
// Create UI elements for custom cursor.
const resultTable = chart
.addUIElement(UILayoutBuilders.Column, {
x: chart.getDefaultAxisX(),
y: chart.getDefaultAxisY(),
})
.setMouseInteractions(false)
.setOrigin(UIOrigins.LeftBottom)
.setMargin(5)
.setBackground((background) =>
background
// Style same as Theme result table.
.setFillStyle(chart.getTheme().resultTableFillStyle)
.setStrokeStyle(chart.getTheme().resultTableStrokeStyle),
)
const rowX = resultTable.addElement(UILayoutBuilders.Row).addElement(UIElementBuilders.TextBox)
const rowsY = series.map((el, i) => {
return resultTable
.addElement(UILayoutBuilders.Row)
.addElement(UIElementBuilders.TextBox)
.setTextFillStyle(series[i].getStrokeStyle().getFillStyle())
})
const tickX = chart.getDefaultAxisX().addCustomTick().setAllocatesAxisSpace(false)
const ticksY = series.map((el, i) => {
return chart
.getDefaultAxisY()
.addCustomTick()
.setAllocatesAxisSpace(false)
.setMarker((marker) => marker.setTextFillStyle(series[i].getStrokeStyle().getFillStyle()))
})
// Hide custom cursor components initially.
// resultTable.dispose()
rowsY.forEach(el=>{
el.dispose()
})
tickX.dispose()
ticksY.forEach((tick) => tick.dispose())
// Implement custom cursor logic with events.
chart.onSeriesBackgroundMouseMove((_, event) => {
const mouseLocationClient = { x: event.clientX, y: event.clientY }
// Translate mouse location to LCJS coordinate system for solving data points from series, and translating to Axes.
const mouseLocationEngine = chart.engine.clientLocation2Engine(mouseLocationClient.x, mouseLocationClient.y)
// Translate mouse location to Axis.
const mouseLocationAxis = translatePoint(mouseLocationEngine, chart.engine.scale, series[0].scale)
// Solve nearest data point to the mouse on each series.
const nearestDataPoints = series.map((el) => el.solveNearestFromScreen(mouseLocationEngine))
// Find the nearest solved data point to the mouse.
const nearestPoint = nearestDataPoints.reduce((prev, curr, i) => {
if (!prev) return curr
if (!curr) return prev
return Math.abs(mouseLocationAxis.y - curr.location.y) < Math.abs(mouseLocationAxis.y - prev.location.y) ? curr : prev
})
if (nearestPoint) {
// Set custom cursor location.
resultTable.setPosition({
x: nearestPoint.location.x,
y: nearestPoint.location.y,
})
// Change origin of result table based on cursor location.
if (nearestPoint.location.x > chart.getDefaultAxisX().getInterval().end / 1.5) {
if (nearestPoint.location.y > chart.getDefaultAxisY().getInterval().end / 1.5) {
resultTable.setOrigin(UIOrigins.RightTop)
} else {
resultTable.setOrigin(UIOrigins.RightBottom)
}
} else if (nearestPoint.location.y > chart.getDefaultAxisY().getInterval().end / 1.5) {
resultTable.setOrigin(UIOrigins.LeftTop)
} else {
resultTable.setOrigin(UIOrigins.LeftBottom)
}
// Format result table text.
rowX.setText(`X: ${chart.getDefaultAxisX().formatValue(nearestPoint.location.x)}`)
rowsY.forEach((rowY, i) => {
rowY.setText(`Y${i}: ${chart.getDefaultAxisY().formatValue(nearestDataPoints[i]?.location.y || 0)}`)
})
// Position custom ticks.
tickX.setValue(nearestPoint.location.x)
ticksY.forEach((tick, i) => {
tick.setValue(nearestDataPoints[i]?.location.y || 0)
})
// Display cursor.
rowX.restore()
series.forEach((el, i)=>{
if(!el.isDisposed()){
rowsY[i].restore()
}
})
tickX.restore()
ticksY.map((el) => el.restore())
} else {
// Hide cursor.
disposeCustomCursor()
tickX.dispose()
ticksY.map((el) => el.dispose())
}
})
chart.onSeriesBackgroundMouseLeave((_, e) => {
disposeCustomCursor()
tickX.dispose()
ticksY.map((el) => el.dispose())
})
chart.onSeriesBackgroundMouseDragStart((_, e) => {
disposeCustomCursor()
tickX.dispose()
ticksY.map((el) => el.dispose())
})
function disposeCustomCursor() {
rowX.dispose()
rowsY.forEach(el=>{
el.dispose()
})
}
setTimeout(() => {
series[0].dispose()
rowsY[0].dispose()
}, 3000);
setTimeout(() => {
series[0].restore()
rowsY[0].restore()
}, 6000);
I am creating a component tree visualisation with Dagre and React-flow, and unfortunately I face some difficulties. The edges are correct, all have the right identifiers for the source and the target, but if I don't use the Handle component provided by react-flow-renderer, the handles (small dots, connecting points for edges) won't appear. Even when I set the element targetPosition and sourcePosition. I think el.targetPosition and el.sourcePosition don't do anything. Most of the implementation below is from React-flow official website, and they don't use Handle components. Handle id is null.
You can also find a snapshot below.
Rendering the elements
{elements && (
<ReactFlowProvider>
<ReactFlow
elements={elems}
nodeTypes={{ reactComponent: ComponentNode }}
onNodeMouseEnter={(_e, node) => highlightComponent(node, false)}
onNodeMouseLeave={(_e, node) => removeHighlight(node)}
onPaneClick={resetHighlight}
>
</ReactFlow>
</ReactFlowProvider>
Rest of the code
const positionElements = (elements, dagreGraph, direction) => {
return elements.forEach((el) => {
if (isNode(el)) {
if (direction === GraphLabels.topToBottom) {
dagreGraph.setNode(el.id, {
width: nodeWidth,
height: baseNodeHeight + el.data.linesOfCode,
});
}
} else {
dagreGraph.setEdge(el.source, el.target);
}
});
};
export const getLayoutedElements = (elements, direction) => {
const dagreGraph = new dagre.graphlib.Graph(); // building the graph
dagreGraph.setDefaultEdgeLabel(() => ({}));
dagreGraph.setGraph({ rankdir: direction });
positionElements(elements, dagreGraph, direction);
dagre.layout(dagreGraph);
return elements.map((el) => {
if (isNode(el)) {
const nodeWithPosition = dagreGraph.node(el.id);
Vertical scaling
if (direction == GraphLabels.leftToRight) {
do this.
}
if (direction == GraphLabels.topToBottom) {
el.targetPosition = 'bottom';
el.sourcePosition = 'top';
el.position = {
x: someValue,
y: someOtherValue,
};
}
}
return el;
});
};
I'm still beginner with CSS and Javascript. I tried to make a carousel using CSS and JavaScript.
I would like to know how do I create the logic for the dots on my custom carousel?
I created the buttons, and they are working to pass the slides. But can you tell me how do I create the dots?
This is my project into codesandbox
export function usePosition(ref) {
const [prevElement, setPrevElement] = React.useState(null);
const [nextElement, setNextElement] = React.useState(null);
React.useEffect(() => {
const element = ref.current;
const update = () => {
const rect = element.getBoundingClientRect();
const visibleElements = Array.from(element.children).filter((child) => {
const childRect = child.getBoundingClientRect();
return rect.left <= childRect.left && rect.right >= childRect.right;
});
if (visibleElements.length > 0) {
setPrevElement(getPrevElement(visibleElements));
setNextElement(getNextElement(visibleElements));
}
};
update();
element.addEventListener("scroll", update, { passive: true });
return () => {
element.removeEventListener("scroll", update, { passive: true });
};
}, [ref]);
const scrollToElement = React.useCallback(
(element) => {
const currentNode = ref.current;
if (!currentNode || !element) return;
let newScrollPosition;
newScrollPosition =
element.offsetLeft +
element.getBoundingClientRect().width / 2 -
currentNode.getBoundingClientRect().width / 2;
console.log("newScrollPosition: ", newScrollPosition);
currentNode.scroll({
left: newScrollPosition,
behavior: "smooth"
});
},
[ref]
);
const scrollRight = React.useCallback(() => scrollToElement(nextElement), [
scrollToElement,
nextElement
]);
return {
hasItemsOnLeft: prevElement !== null,
hasItemsOnRight: nextElement !== null,
scrollRight,
scrollLeft
};
}
Thank you in advance for any help!!
below you will find my solution, I took your code from the sandbox and worked with it. I didn't understand if you wanted to show the dots, to scroll and sync the dots and click on the dots to change the image, so I did all of them .
codesandbox
Hello! I triyng to complete the functionality displayed on pinned image. I have done this with FlatList's onScroll event, but it change value to slow, active item not always at the middle. Is this some way to always give styles to item, which is on center? Below is my scroll handler.
const onScroll = (event) => {
const { y } = event.nativeEvent.contentOffset;
const { height: layoutHeight } = event.nativeEvent.layoutMeasurement;
const value = Math.floor(y / itemHeight) + Math.floor(layoutHeight / itemHeight / 2);
if (activeItem !== value) {
setActiveItem(value);
}
}
Try to add async/await before setting the active item value
const onScroll = async (event) => {
await const { y } = event.nativeEvent.contentOffset;
await const { height: layoutHeight } = event.nativeEvent.layoutMeasurement;
await const value = Math.floor(y / itemHeight) + Math.floor(layoutHeight / itemHeight / 2);
await if (activeItem !== value) {
setActiveItem(value);
}
}
I have a function that calculates bounds:
function Canvas() {
this.resize = (e) => {
this.width = e.width;
this.height = e.height;
}
this.responsiveBounds = (f) => {
let cached;
return () => {
if (!cached) {
cached = f(this);
}
return cached;
};
}
}
and I have a function that uses this Canvas object bounds:
function Box(canvas) {
let fBounds = canvas.responsiveBounds(({ width, height }) => {
return {
width,
height
};
});
this.render = () => {
let bounds = fBounds();
console.log(bounds);
};
}
Now when canvas resize function is called, bounds will change, and I need to reflect this change (by clearing the cached variable somehow). I can't make cached global because responsiveBounds is called by multiple users.
I see two different options:
1. Register listeners to the change
function Canvas() {
let listeners = [];
this.resize = (e) => {
this.width = e.width;
this.height = e.height;
listeners.forEach(listener => listener());
};
this.responsiveBounds = (f) => {
let cached;
listeners.push(() => cached = undefined);
return () => {
if (!cached) {
cached = f(this);
}
return cached;
};
}
}
Here each responsiveBounds execution context has its own listener registered, so it can take care of clearing its private cache.
2. Collect all caches in a Weak Map
With a (Weak) Map you can create a store for private cache variables, keyed by the function object for which they are relevant:
function Canvas() {
let cached = new WeakMap;
this.resize = (e) => {
this.width = e.width;
this.height = e.height;
cached.clear();
};
this.responsiveBounds = (f) => {
return () => {
let result = cached.get(f);
if (result === undefined) {
cached.set(f, result = f(this));
}
return result;
};
}
}
The nice thing about Weak Maps is that if the function returned by responsiveBounds gets garbage collected, and also the corresponding f, then the corresponding WeakMap entry can be garbage collected as well.
The advantage of the first solution is that the resize method does not need to know anything about what the listeners are doing (whether they are dealing with a cache or entirely something else).