How to redraw text rendered using SVGRenderer in Highcharts React? - reactjs

I am using the SVGRenderer to draw the total value in the center of donut chart shown below:
The code to do this is shown below (please see CodeSandbox here)
export const PieChart = ({ title, totalLabel, pieSize, pieInnerSize, data }: PieChartProps) => {
const chartRef = useRef(null);
const [chartOptions, setChartOptions] = useState(initialOptions);
useEffect(() => {
// set options from props
setChartOptions({...});
// compute total
const total = data.reduce((accumulator, currentValue) => accumulator + currentValue.y, 0);
// render total
const totalElement = chart.renderer.text(total, 0, 0).add();
const totalElementBox = totalElement.getBBox();
// Place total
totalElement.translate(
chart.plotLeft + (chart.plotWidth - totalElementBox.width) / 2,
chart.plotTop + chart.plotHeight / 2
);
...
}, [title, totalLabel, pieSize, pieInnerSize, data]);
return (
<HighchartsReact
highcharts={Highcharts}
containerProps={{ style: { width: '100%', height: '100%' } }}
options={chartOptions}
ref={chartRef}
/>
);
};
However this approach has two issues:
When chart is resized, the total stays where it is - so it is no longer centered inside the pie.
When the chart data is changed (using the form), the new total is drawn over the existing one.
How do I solve these issues? With React, I expect the chart to be fully re-rendered when it is resized or when the props are changed. However, with Highcharts React, the chart seems to keep internal state which is not overwritten with new props.

I can suggest two options for this case:
Use the events.render callback, destroy and render a new label after each redraw:
Demo: https://jsfiddle.net/BlackLabel/ps97bxkg/
Use the events.render callback to trasnlate those labels after each redraw:
Demo: https://jsfiddle.net/BlackLabel/6kwag80z/
Render callback triggers after each chart redraw, so it is fully useful in this case - more information
API: https://api.highcharts.com/highcharts/chart.events.render

I'm not sure if this helps, but I placed items inside of highcharts using their svgrenderer functionality (labels, in particular, but you can also use the text version of svgrenderer), and was able to make them responsive
I was able to do something by manually changing the x value of my svgRenderer label.
I'm in angular and have a listener for screen resizing:
Also Note that you can change the entire SVGRenderer label with the attr.text property.
this.chart = Highcharts.chart(.....);
// I used a helper method to create the label
this.chart.myLabel = this.labelCreationHelperMethod(this.chart, data);
this.windowEventService.resize$.subscribe(dimensions => {
if(dimensions.x < 500) { //this would be your charts resize breakpoint
// here I was using a specific chart series property to tell where to put my x coordinate,
// you can traverse through the chart object to find a similar number,
// or just use a hardcoded number
this.chart.myLabel.attr({ y: 15, x: this.chart.series[0].points[0].plotX + 20 });
} else {
this.chart.myLabel.attr({ y: 100, x: this.chart.series[0].points[0].plotX + 50 });
}
}
//Returns the label object that we keep a reference to in the chart object.
labelCreationHelperMethod() {
const y = screen.width > 500 ? 100 : 15; 
const x = screen.width > 500 ? this.chart.series[0].points[0].plotX + 50 :
this.chart.series[0].points[0].plotX + 20
// your label
const label = `<div style="color: blue"...> My Label Stuff</div>`
return chart.renderer.label(label, x, y, 'callout', offset + chart.plotLeft, chart.plotTop + 80, true)
.attr({
fill: '#e8e8e8',
padding: 15,
r: 5,
zIndex: 6
})
.add();
}

Related

How to keep my mapview.markers from rescaling when calling setGlobalState?

So i am using react native or more specifically expo mapview to render a mapview component with some custom markers. Theese markers are then connected to a scrollview in a way such that as when the scrollview is scrolled different markers corresponding to what is shown is then rescaled, aka gets 1.5x bigger this is also periodic so when im in between two they are both like 1.25x bigger code for this map with markers show below.
<MapView
ref={refMap}
style={styles.map}
region={state.region}
scrollEnabled={true}
zoomEnabled={true}
zoomTapEnabled={false}
rotateEnabled={true}
showsPointsOfInterest={false}
moveOnMarkerPress={false} // android only
minZoomLevel={13}
maxZoomLevel={16}
onMapReady={(resetMap)}
//onMarkerPress={onMarkerPress}
>
{/* creates markers on map with unique index; coordinates, img, title from mapData*/}
{state.markers.map((marker, index) => {
const scaleStyle = { // creates a scaleStyle by transforming/scaling the marker in func interpolations
transform: [
{
scale: interpolations[index].scale,
},
],
};
return (
<MapView.Marker
key={index}
coordinate={marker.coordinate}
onPress={onMarkerPress}
>
<Animated.View style={styles.markerWrap}>
<Animated.Image
source={marker.image}
style={[styles.marker, scaleStyle]} // adding the scaleStyle to the marker img (which is the marker)
//resizeMode= 'cover'
>
</Animated.Image>
</Animated.View>
</MapView.Marker>
);
})}
</MapView>
//function codes seen here
let markerAnimation = new Animated.Value(0);
//console.log(markerAnimation)
// Called when creating the markers (markers and cards has matching indexes)
// Setting inputRange (card index/position) for the animation, mapping it to an outputRange (how the marker should be scaled)
const interpolations = state.cards.map((card, index) => {
const inputRange = [
(index - 1) * Dimensions.get('window').width,
index * Dimensions.get('window').width,
((index + 1) * Dimensions.get('window').width),
];
const scale = markerAnimation.interpolate({
inputRange, //card index/positions [before, present, after]
outputRange: [1, 1.5, 1], //scales the marker [before, present, after]
extrapolate: "clamp"
});
return { scale };
});
// Called when a marker is pressed
const onMarkerPress = (mapEventData) => {
const markerIndex = mapEventData._targetInst.return.key; // returns the marker's index
let x = (markerIndex * Dimensions.get('window').width); // setting card x-position
refCards.current.scrollTo({x: x, y: 0, animated: true}); // scroll to x-position with animation
};
And whilst this whole interaction has been working exactly like i imagined i have this button that takes me to another page in the app which on arrival needs to fetch some data trough an api call. and this data depends on the index of the scrollview. In this other screen where the scrollview is also present i just use the onMomentumScrollEnd to set a global state value to the corresponding value(code for this global state shown below) this works but i would like to do exacty the same thing in the screen containing the map. but whenever a global state change is called the scaling "resets" so that the first one is the one "highlighted" even tho the scrollview is somewhere completly different.
//this is a index.js containing the store
import { createGlobalState } from 'react-hooks-global-state';
const region = {
latitude: 59.85843,
longitude: 17.63356,
latitudeDelta: 0.01,
longitudeDelta: 0.0095,
}
const { setGlobalState, useGlobalState } = createGlobalState({
currentIndex: 0,
initialMapState: {
markers: [],
region: region,
cards: [],
}
}
)
//and in a screen this can be used to change/read
import {setGlobalState, useGlobalState} from '../state' //imports it
const [currentIndex] = useGlobalState("currentIndex") //reads value
//and this is how i use the onMomentumScrollend
const onMomentumScrollEnd = ({ nativeEvent }) => {
const index = Math.round(nativeEvent.contentOffset.x / Dimensions.get('window').width);
if (index !== currentIndex) {
setGlobalState('currentIndex',index)
}
};
Any help is greatly appreciated as well im simply lost, i have tried storing the index in some sort of ref which worked fine and then try to update it on some sort of callback on the useFocusEffect but this got buggy real fast and ended up not really working. I have also tried to google to find some sort of way to keep the markers from not rerendering on state change, something which seems impossible? altough i think it would solve my problem as the markers dont depend on the state at all.
*edit
I also feel like i forgot to add that i am extremly happy to switch to any other sort of global state manager if it contains some sort of way to keep this fromh happening
**edit
So after revising this problem i found a soloution which i am not happy with, but is working.
useEffect(() => {
//scrolls back without animation when currentIndex is changed to handle react autorerender
//And scrolls if value is changed in eventscreen
refCards.current.scrollTo({
x: currentIndex * Dimensions.get("window").width,
y: 0,
animated: false,
});
}, [currentIndex]);

FabricJS object is not selectable/clickable/resizable in top left corner (React + FabricJs)

I have FabricJs canvas. And I have multiple objects outside canvas. When I click on object, it appears in Canvas. I am not doing anything else with this object programmatically.
Here is part of code that add image to Canvas (it is React code):
const { state, setState } = React.useContext(Context);
const { canvas } = state;
...
const handleElementAdd = (event, image) => {
const imageSize = event.target.childNodes[0].getBoundingClientRect();
const imageUrl = `/storage/${image.src}`;
new fabric.Image.fromURL(imageUrl, img => {
var scale = 1;
if (img.width > 100) {
scale = 100/img.width;
}
var oImg = img.set({
left: state.width / 2 - img.width * scale,
top: state.height / 2 - img.height * scale,
angle: 0})
.scale(scale);
canvas.add(oImg).renderAll.bind(canvas);
canvas.setActiveObject(oImg);
});
};
...
<div
key={img.name}
onClick={(e) => handleElementAdd(e, img)}
className={buttonClass}
>
It appears, I can drag and drop, resize, select it. However, when my object is placed in top left corner of canvas, it is not clickable, resizable etc. I can select larger area and then object is selected, but it doesn't respond to any events like resize, select etc.
Not sure where to look at.
I read that if coordinates are changed programmatically object.setCoord() to object can help, but in this case I do not understand where to put it.
You have to put (call) object.setCoords() each time you have modified the object, or moved the object. So, you can call it on mouseUp event of the canvas.
You can write a function naming "moveObject" in which you get the object you are trying to move/moved/updated. And then just selectedObject.setCoords().
Then call that function inside canvas.on('selection:updated', e => {----})

Pop Out SVG US State Feature Path On Click

I'm rendering a geoAlbersUSA SVG map using d3-geo and topojson-client - as shown below - but I'm trying to do something fancy when a user clicks on the path. I would like to scale and translate the underlying path, in this case Idaho or Virginia, so that the state "floats" above the United States, but is still centered on the centroid. Every time I try to simply scale the <path/> element it ends up many pixels away; is there a way to calculate a translate() that corresponds to how much you have scaled the element?
const states = topojson.feature(us, us.objects.states as GeometryObject) as ExtendedFeatureCollection;
const path = geoPath()
.projection(geoAlbersUsa()
.fitSize([innerWidth as number, innerHeight as number], states)
);
<svg>
{
states.features.map(feature => {
return <path key={`state-path-${feature.id}`} d={path(feature) as string} onClick={() => updateScaleAndTranslate(feature)} />
}
}
</svg>
You need to set the transform-origin style property. It determines the point relative to which the transformations are applied. If you can find the centre of the path (for example, using getBBox()), and then set
.style("transform-origin", `${myCenter.x}px ${myCenter.y}px`)
This should change the way the transform is being applied.
I made a small example for you, using d3 v6. Note that as states are "expanded" they might now start to overlap, so functionally, I'd recommend to allow only one or a few states to be expanded like that.
const svg = d3.select('body')
.append("svg")
.attr("viewBox", [0, 0, 975, 610]);
d3.json("https://cdn.jsdelivr.net/npm/us-atlas#3/states-albers-10m.json").then((us) => {
const path = d3.geoPath()
const color = d3.scaleQuantize([1, 10], d3.schemeBlues[9]);
const statesContainer = svg.append("g");
const states = statesContainer
.selectAll("path")
.data(topojson.feature(us, us.objects.states).features)
.join("path")
.attr("class", "state")
.attr("fill", () => color(Math.random() * 10))
.attr("d", path)
.on("click", function(event, d) {
const {
x,
y,
width,
height
} = this.getBBox();
// First, move the state to the front so it's in front of others
const state = d3.select(this)
.attr("transform-origin", `${x + width / 2}px ${y + height / 2}px`).remove();
statesContainer.append(() => state.node());
d.properties.expanded = !d.properties.expanded;
state
.transition()
.duration(500)
.attr("transform", d.properties.expanded ? "scale(1.25)" : "scale(1)")
});
states
.append("title")
.text(d => d.properties.name)
});
.state {
stroke: #fff;
stroke-linejoin: round;
stroke-width: 2px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.js"></script>
<script src="https://d3js.org/topojson.v3.js"></script>

How to develop responsive layer container in Leaflet?

I'm using leaflet.js to develop a drawing layer tool with the full-page map. Users will draw a group of layers to describe an industrial product's parts.
The leaflet map and its layers should be fit to screen. For example, if a user draws a circle to the left bottom corner with 1920x1080 resolution pc, I should see it in the left bottom corner with my 1366x768 resolution pc. The leaflet map is resizing with the page correctly but the circle is being out of the screen in my pc because of its latitude-longitude in this example.
I tried to develop this example with invalidateSize, maxBounds, fitBounds, etc. but haven't succeeded yet. How can I develop this scenario?
Saved GeoJSON from 1920x1080 with layers looking:
Loaded same GeoJSON from 1366x768 looking (attention to left corner):
Map (React):
const corner1 = L.latLng(-180, -180);
const corner2 = L.latLng(180, 180);
const bounds = L.latLngBounds(corner1, corner2);
<Map
ref={map => {
this.tryToCreate(map);
}}
className="c-leaflet-map-map"
center={[0, 0]}
zoom={0}
maxZoom={10}
zoomControl={false}
onzoomend={this.zoomLevelsChange}
maxBounds={bounds}
maxBoundsViscosity={1.0}
zoomSnap={0}
crs={L.CRS.Simple}
>
<ImageOverlay
bounds={map.backgroundImageBounds || map.bounds}
attribution="&copy TofnaTech"
url={
map.backgroundImage ||
map.backgroundImageURL ||
TransparentImage
}
opacity={map.opacity}
className={cn({ transparentImage: !map.backgroundImage })}
/>
<FeatureGroup
ref={controller => {
this.controller = controller;
this.tryToCreate();
}}
>
<EditControl
position="topleft"
onCreated={this._onCreated}
onEditStart={this.onEditStart}
onEditStop={this.onEditStop}
onEdited={this.onEdited}
draw={{
polyline: false,
polygon: {
shapeOptions: { ...defaultShapeOptions }
},
rectangle: {
shapeOptions: { ...defaultShapeOptions }
},
circle: {
shapeOptions: { ...defaultShapeOptions }
},
marker: false,
circlemarker: false
}}
edit={{
remove: false
}}
/></Map>
Using React-Leaflet is a little different. You can accomplish what you're trying to do in componentDidMount, where you can make a reference to your map and then call fitBounds on that.
Edit: Adding the rectangle
However, the next issue is resolution. Leaflet will maintain the aspect ratio so you actually need to render your circle (or rectangle)in the corner dynamically if you always want it to show up in the bottom left corner of the screen regardless of the aspect ratio of the screen and browser. So in componentDidMount, just after you set the new bounds, you can get those bounds, and define a rectangle (or anything) to always render in the same spot relative to say, the getBounds()._southWest bound. In this code I wrapped this code in a setTimeout because the animation takes a moment and that moment needs to pass to get the new bounds values from getBounds(). I'm sure there's a way to reduce or remove that animation time.
To keep the rectangle in the same spot, I wrote a onMove function for the map to always recalculate the position regardless of moving the map around or resizing the window.
Note that the size of the rectangle is a little slapdash in this example, but I think you'll have an easier time getting a consistent size using CRS, as CRS is a lot more uniform than lats and lngs.
class Map extends React.Component {
state = {
rectCoords: null
};
componentDidMount() {
const map = this.mapReference.leafletElement;
window.map = map;
console.log("Initiates with bounds:", map.getBounds());
map.fitBounds([[32.852169, -90.5322], [33.852169, -85.5322]]);
setTimeout(() => {
console.log("Bounds immediately reset to", map.getBounds());
const rectCoords = [
[
map.getBounds()._southWest.lat + 0.1,
map.getBounds()._southWest.lng + 0.1
],
[
map.getBounds()._southWest.lat + 1,
map.getBounds()._southWest.lng + 1
]
];
this.setState({ rectCoords });
}, 500);
}
onMapmove = () => {
const map = this.mapReference.leafletElement;
const rectCoords = [
[
map.getBounds()._southWest.lat + 0.1,
map.getBounds()._southWest.lng + 0.1
],
[map.getBounds()._southWest.lat + 1, map.getBounds()._southWest.lng + 1]
];
this.setState({ rectCoords });
}
render() {
return (
<LeafletMap
ref={mapReference => this.mapReference = mapReference}
zoom={4}
center={[33.852169, -100.5322]}
onMove={() => this.onMapmove() } >
<TileLayer ... />
{this.state.rectCoords && (
<Rectangle
bounds={this.state.rectCoords}
color={"#ff7800"}
weight={1}
/>
)}
</LeafletMap>
);
}
}
So even though the map initiates with certain bounds, it immediately fits it to your desired bounds, and renders the rectangle always in the bottom left corner in the same spot.
Here is a working codesandbox. I'm sure this can be adapted to using CRS Simple as well.

Animating a height value for text input

So i am using react-native-autogrow-textinput in order to have an editable document viewable in my application. I am currently trying to work around the keyboard in order to adjust the height of textinput box, so that all text is visible. I have found the following code to do so
componentWillMount () {
this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', this.keyboardDidShow.bind(this));
this.keyboardDidHideListener = Keyboard.addListener('keyboardDidHide', this.keyboardDidHide.bind(this));
}
componentWillUnmount () {
this.keyboardDidShowListener.remove();
this.keyboardDidHideListener.remove();
}
keyboardDidShow(e){
let newSize = Dimensions.get('window').height- e.endCoordinates.height - 150;
console.log(e.endCoordinates);
this.setState({docViewHeight: newSize});
}
keyboardDidHide(e){
let newSize = Dimensions.get('window').height - 170;
this.setState({docViewHeight: newSize})
}
However, the result i am getting is: When the keyboard is animating off screen, the height of the textinput remains the same, let newSize = Dimensions.get('window').height- e.endCoordinates.height - 150, untill the keyboard has finished sliding off screen.
The height then adjusts to fill the whole screen again, except it sort of 'pops' into the new height. How do i get the value of this height to gradually grow, so it looks like it is simply extending to fit the whole screen? Ill post my Autogrow TextInput code below also. Any help would be much appreciated.
<AutoGrowingTextInput
ref="editText"
editable = {this.state.editting}
style = {{fontSize: fontProperties.fontSize+3, marginLeft: 18, marginRight: 18, marginTop: 15}}
/*animate this*/ minHeight = {this.state.docViewHeight}
animation = {{animated: true, duration: 300}}
//has some other confidential props here for onChange etc
</AutoGrowingTextInput>
Found the answer myself, after digging through some library files.
The solution is to use a keyboardWillHide event listener instead of keyboardDidHide.
This will fire before the keyboard begins its outro animation. Ive put the code below.
componentWillMount () {
this.keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', this.keyboardDidShow.bind(this));
this.keyboardWillHideListener = Keyboard.addListener('keyboardWillHide', this.keyboardWillHide.bind(this));
}
componentWillUnmount () {
this.keyboardDidShowListener.remove();
this.keyboardWillHideListener.remove();
}
keyboardWillHide(e){
let newSize = Dimensions.get('window').height - 170;
this.setState({docViewHeight: newSize})
}

Resources