I'm using d3fc+canvas to render candlesticks to a chart and change their fill color based on whether it's an up or down candle. The series generator looks like this
const generator = fc.seriesCanvasCandlestick()
.crossValue(d => d.date)
.openValue(d => d.open)
.highValue(d => d.high)
.lowValue(d => d.low)
.closeValue(d => d.close)
.decorate((context, datum, index) => {
context.fillStyle = datum.close > datum.open ? 'blue' : 'red'
});
I'm now trying to convert this to webGl rendering (using seriesWebglCandlestick) but I don't know how to set the fill color in the same way.
How can I convert my decorate function to color the webgl candlesticks?
It seems like I can get the Webgl context using context.context() but setting the fillStyle on it doesnt do anything.
I saw another stackoverflow answer suggest:
renderer.setClearColor(0xcc3dca, 0.7)
renderer.clear()
But there seems to be no setClearColor function on the Webgl context (though I do see clear())
I also do see clearColor() which expects 4 arguments so I tried passing rgba values like clearColor(197,197,197,0.7) but also has no effect.
Now this functionality has been released, the code should look like this -
const fillColor = fc
.webglFillColor()
.data(data)
.value(d =>
d.close > d.open ?
[0, 0, 1, 1] : [1, 0, 0, 1]
);
const series = fc
.seriesWebglCandlestick()
.xScale(xScale)
.yScale(yScale)
.context(gl)
.crossValue((_, i) => i)
.highValue(d => d.high)
.lowValue(d => d.low)
.openValue(d => d.open)
.closeValue(d => d.close)
.decorate(program => {
fillColor(program);
});
N.B. webglFillColor will have no effect if there's no fill e.g. for ohlc. In that case you'll need to use webglStrokeColor .
Original answer below.
Unfortunately this isn't currently easy with the WebGL series in d3fc but we do expect that to change shortly.
As it stands this requires delving pretty deeply into the internals of the d3fc-webgl package. We don't recommend doing this currently (until the issue above is fixed) because the naming and exact functionality of components is liable to change.
However, if you're very keen to try this out here's the code required in the current version -
This code will only run if you build your own version of the library to export elementConstantAttributeBuilder from packages/d3fc-webgl/index.js
const fillColorAttribute = fc
.elementConstantAttributeBuilder()
.size(4)
.data(data)
.value((data, element) =>
data[element].close > data[element].open ?
[0, 0, 1, 1] : [1, 0, 0, 1]
);
const series = fc
.seriesWebglCandlestick()
.xScale(xScale)
.yScale(yScale)
.context(gl)
.crossValue((_, i) => i)
.highValue(d => d.high)
.lowValue(d => d.low)
.openValue(d => d.open)
.closeValue(d => d.close)
.decorate(program => {
program
.vertexShader()
.appendHeader('attribute vec4 aFillColor;')
.appendHeader('varying vec4 vFillColor;')
.appendBody('vFillColor = aFillColor;');
program
.fragmentShader()
.appendHeader('varying vec4 vFillColor;')
.appendBody('gl_FragColor = vFillColor;');
program
.buffers()
.attribute('aFillColor', fillColorAttribute);
});
Which produces the following -
Related
What I need
Let's start with The mentions plugin taken from the docs.
I would like to enhance if with the following functionality:
Whenever I click on an existing MentionNode, the menu gets rendered (like it does when menuRenderFunction gets called), with the full list of options, regardless of queryString matching
Selecting an option from menu replaces said mention with the newly selected one
Is there a way to implement this while leaving LexicalTypeaheadMenuPlugin in control of the menu?
Thank you for your time 🙏🏻
What I've tried
I figured that maybe I could achieve my desired behaviour simply by returning the right QueryMatch from triggerFn. Something like this:
const x: FC = () => {
const nodeAtSelection = useNodeAtSelection() // Returns LexicalNode at selection
return (
<LexicalTypeaheadMenuPlugin<VariableTypeaheadOption>
triggerFn={(text, editor) => {
if ($isMentionsNode(nodeAtSelection)) {
// No idea how to implement `getQueryMatchForMentionsNode`,
// or whether it's even possible
return getQueryMatchForMentionsNode(nodeAtSelection, text, editor)
}
return checkForVariableBeforeCaret(text, editor)
}}
/>
)
}
I played around with it for about half an hour, unfortunately I couldn't really find any documentation for triggerFn or QueryMatch, and haven't really made any progress just by messing around.
I also thought of a potential solution the I think would work, but feels very hacky and I would prefer not to use it. I'll post it as an answer.
So here is my "dirty" solution that should work, but feels very hacky:
I could basically take the function which I provide to menuRenderFn prop and call it manually.
Let's say I render the plugin like this:
const menuRenderer = (
anchorElementRef,
{ selectedIndex, selectOptionAndCleanUp, setHighlightedIndex }
) => { /* ... */}
return (
<LexicalTypeaheadMenuPlugin menuRenderFn={menuRenderer} /* ... other props */ />
)
I could then create a parallel environment for rendering menuRenderer, something like this:
const useParallelMenu = (
menuRenderer: MenuRenderFn<any>,
allOptions: TypeaheadOption[],
queryString: string
) => {
// I could get anchor element:
// 1. either by using document.querySelector("." + anchorClassName)
// 2. or by extracting it from inside `menuRenderFn`:
// menuRenderFn={(...params) => {
// extractedRef.current = params[0].current;
// return menuRenderer(...params)
// }}
const anchorEl = x
const [selectedIndex, setHighlightedIndex] = useState(0)
const nodeAtSelection = useNodeAtSelection() // Returns LexicalNode at selection
const selectOptionAndCleanUp = (option: TypeaheadOption) => {
// Replace nodeAtSelection with new MentionsNode from `option`
}
return () =>
$isMentionsNode(nodeAtSelection) &&
menuRenderer(
anchorEl,
{
selectedIndex,
setHighlightedIndex,
selectOptionAndCleanUp,
options: allOptions
},
queryString
)
}
On paper, this seems like a viable approach to me... but I would really prefer not to have to do this and instead let LexicalTypeaheadMenuPlugin manage the state of my menu, as it is intended to do.
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]);
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();
}
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>
I am working through the Flavio Copes Pixel Art app in his React course and I'd like to add a button that can resize my "canvas". The canvas is a 2D array that is initialized like so:
const [matrix, setMatrix] = useState(
Array(size)
.fill()
.map(() => Array(size).fill(0)));
The size variable is passed down as a prop from the parent component, which has a hook to manage the state from an input field. The matrix is used to render "Pixel" components (30px x 30px divs) by mapping through each row/col like so:
<div className={css(styles.canvas)}>
{matrix.map((row, rowIndex) =>
row.map((_, colIndex) => {
return (
<Pixel
key={`${rowIndex}-${colIndex}`}
background={Colors[matrix[rowIndex][colIndex]]}
onClick={() => changeColor(rowIndex, colIndex)}
/>
);
})
)}
</div>
I am able to update the width of the canvas (# of columns), but I think that the number of total Pixels remain constant and subsequent rows are added/subtracted that do not equal the number of columns. You can replicate the error by inputting a new value into "Canvas Size" and clicking "Resize Canvas" on my demo.
Why do the number of rows differ from the number of columns?
All of the code is available in my repo.
I do not see change of columns/rows in your repo. This function should do the trick
const updateMatrixSize = (size) => {
setMatrix(
matrix.length > size
? matrix.slice(0, size).map(it => it.slice(0, size)))
: [
...matrix.map(it => ([
...it,
...new Array(size - matrix.length).fill(0)
])
),
...new Array(size - matrix.length).fill(0)
]
)
}
PS you have a typo in your repo
props.setMsatrixState(newMatrix);