how to add Inverted triangle shape scatter in rechart - reactjs

I'm using Scatter chart using rechart and I'm using shape of scatter as "triangle", My requirement is Inverted trianle. I tried using angle but it's not working .can anyone help me in this, How to draw inverted traingle.May be we can write custom shape using rect or path, but I'm new to this,please someone help me .
code:
const {ScatterChart, Scatter, XAxis, YAxis, ZAxis, CartesianGrid, Tooltip, Legend} = Recharts;
const data01 = [{x: 100, y: 200}, {x: 120, y: 0},
];
const ThreeDimScatterChart = React.createClass({
render () {
return (
<ScatterChart width={400} height={400} margin={{top: 20, right: 20, bottom: 20, left: 20}}>
<XAxis type="number" dataKey={'x'} name='stature' unit='cm'/>
<YAxis type="number" dataKey={'y'} name='weight' unit='kg'/>
<CartesianGrid />
<Tooltip cursor={{strokeDasharray: '3 3'}}/>
<Legend />
<Scatter name='A school' data={data01} fill='#8884d8' shape="triangle"/>
</ScatterChart>
);
}
})
ReactDOM.render(
<ThreeDimScatterChart />,
document.getElementById('container')
);
Thanks

You should create a custom shape for the scatter using polygon or your own SVG.
Example with polygon:
<Scatter name="A school" data={data01} fill="#8884d8" shape="<TriangleShape angle={180} />" />;
const TriangleShape = (props: any): JSX.Element => {
return <polygon {...props} points={calcTrianglePoints(props.cx, props.cy, props.r * 2, props.angle || 0)} />;
};
/** *
* Calc points of the triangle
*
* #param xCenter The X-coordinate of the center of triangle
* #param yCenter The Y-coordinate of the center of triangle
* #param sideLength The length of the side
* #param angle The angle in degrees
*/
const calcTrianglePoints = (xCenter: number, yCenter: number, sideLength: number, angle: number = 0): string => {
const r = (Math.sqrt(3) / 3) * sideLength; // The radius of the circumscribed circle
const h = (Math.sqrt(3) / 2) * sideLength; // The height of median
const angleInRadian = (Math.PI * angle) / 180;
const pointCenter: [number, number] = [xCenter, yCenter];
const pointCenterNew: [number, number] = rotatePoint(pointCenter, angleInRadian);
// 1st point
const point1 = movePointToNewCoordinates(
rotatePoint([xCenter, yCenter - r], angleInRadian),
pointCenterNew,
pointCenter
);
const x1 = point1[0];
const y1 = point1[1];
// 2nd point
const point2 = movePointToNewCoordinates(
rotatePoint([xCenter - sideLength / 2, yCenter + (h - r)], angleInRadian),
pointCenterNew,
pointCenter
);
const x2 = point2[0];
const y2 = point2[1];
// 3rd point
const point3 = movePointToNewCoordinates(
rotatePoint([xCenter + sideLength / 2, yCenter + (h - r)], angleInRadian),
pointCenterNew,
pointCenter
);
const x3 = point3[0];
const y3 = point3[1];
return `${x1},${y1},${x2},${y2},${x3},${y3}`;
};
/** *
* Move point to the new coordinate regarding center
*
* #param point The coordinate of point
* #param center The new center
* #param centerOld The old center
*/
const movePointToNewCoordinates = (
point: [number, number],
center: [number, number],
centerOld: [number, number]
): [number, number] => {
const x = point[0];
const y = point[1];
const xCenter = center[0];
const yCenter = center[1];
const xCenterOld = centerOld[0];
const yCenterOld = centerOld[1];
return [x - xCenter + xCenterOld, y - yCenter + yCenterOld];
};
/** *
* Rotate point
*
* #param point The coordinate of point
* #param center The angle in radians
*/
const rotatePoint = (point: [number, number], angle: number): [number, number] => {
const x = point[0];
const y = point[1];
const xNew = x * Math.cos(angle) - y * Math.sin(angle);
const yNew = x * Math.sin(angle) + y * Math.cos(angle);
return [xNew, yNew];
};

Related

How to change the shape of Points from ThreeJS from a square into a circle?

I am using React, Three JS, React-Three-Fibre
Here is what I have:
// Texture for Points
const getTexture = (size, color) => {
const matCanvas = document.createElement('canvas');
matCanvas.width = matCanvas.height = size;
const matContext = matCanvas.getContext('2d');
const texture = new THREE.Texture(matCanvas);
const center = size / 2;
matContext.beginPath();
matContext.arc(center, center, size/2, 0, 2 * Math.PI, false);
matContext.closePath();
matContext.fillStyle = color;
matContext.fill();
//texture.minFilter = THREE.NearestFilter;
texture.needsUpdate = true;
return texture;
}
const vertices = [];
for ( let i = 0; i < 10; i ++ ) {
const x = THREE.MathUtils.randFloatSpread( 10 );
const y = THREE.MathUtils.randFloatSpread( 10 );
const z = THREE.MathUtils.randFloatSpread( 10 );
vertices.push( x, y, z );
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute( 'position', new THREE.Float32BufferAttribute( vertices, 3 ) );
const material = new THREE.PointsMaterial( { color: 0x123684 } );
const points = new THREE.Points( geometry, material );
And here is the R3F
return (
<points args={[geometry]}>
<pointsMaterial
size={1}
map={getTexture(10, 'red')}
/>
</points>
)
The problem is instead of the points changing shape from square to circle, instead they are the picture of a circle inside of a square
enter image description here

How to fill a box with boxGeometry randomly and no overlap?

I am using THREE.js in react.
I want to fill 500 number of 1X1X1 box geometry in a 200x200x200 area randomly.
The following code shows how to get the position.xyz with the range from -100 to 100.
However, how to get the position that all 1X1X1 box geometry could not touch each other (no overlapping)?
I have no idea.
function Geometry() {
const geo_num = 500;
const box_length = 200;
const geo_position = useMemo(() => {
const array = []
for (let i = 0; i < geo_num; i++) {
const xdirection = Math.round(Math.random()) * 2 - 1;
const ydirection = Math.round(Math.random()) * 2 - 1;
const zdirection = Math.round(Math.random()) * 2 - 1;
const xpos = Math.random() * box_length * xdirection * 0.5;
const ypos = Math.random() * box_length * ydirection * 0.5;
const zpos = Math.random() * box_length * zdirection * 0.5;
array.push(new THREE.Vector3(xpos, ypos, zpos))
}
return array
})
return (
<>
{
geo_position.map((element, index) => (
<mesh position={element} key={index} >
<boxGeometry args={[1, 1, 1]} />
</mesh>
))
}
</>
)
}
A very rough concept of how you can do it, using an array and its .includes() method:
body{
overflow: hidden;
margin: 0;
}
<script type="module">
import * as THREE from "https://cdn.skypack.dev/three#0.136.0";
import {OrbitControls} from "https://cdn.skypack.dev/three#0.136.0/examples/jsm/controls/OrbitControls";
let scene = new THREE.Scene();
let camera = new THREE.PerspectiveCamera(60, innerWidth / innerHeight, 1, 1000);
camera.position.set(50, 50, 250);
let renderer = new THREE.WebGLRenderer({antialias: true});
renderer.setSize(innerWidth, innerHeight);
document.body.appendChild(renderer.domElement);
window.addEventListener("resize", event => {
camera.aspect = innerWidth / innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
let contols = new OrbitControls(camera, renderer.domElement);
let boxCount = 500;
let range = [-100, 100];
let v3Array = [];
let counter = 0;
let v3 = new THREE.Vector3();
while(counter < boxCount){
let v3 = [
THREE.MathUtils.randInt(range[0], range[1]).toFixed(0),
THREE.MathUtils.randInt(range[0], range[1]).toFixed(0),
THREE.MathUtils.randInt(range[0], range[1]).toFixed(0),
].join("|");
if (!v3Array.includes(v3)){
v3Array.push(v3);
counter++
}
}
v3Array.map( p => {
let o = new THREE.Mesh(new THREE.BoxGeometry(), new THREE.MeshBasicMaterial({color: Math.random() * 0xffffff, wireframe: true}));
let pos = p.split("|").map(c => {return parseInt(c)});
o.position.set(pos[0], pos[1], pos[2]);
scene.add(o);
});
let boxHelper = new THREE.Box3Helper(new THREE.Box3(new THREE.Vector3().setScalar(range[0] - 0.5), new THREE.Vector3().setScalar(range[1] + 0.5)), "yellow");
scene.add(boxHelper);
renderer.setAnimationLoop( () => {
renderer.render(scene, camera);
})
</script>

Here's a go, above occurred in symbol component. (Symbol.tsx: undefined is not a constructor (evaluating 'new _simplexNoise.default(i + "-" + j)') )

construct.js infraction, coupled in with the source at Symbol.tsx, likely.
I've been lurking for so many years ya it's cherry time, utterly been helping young wees like me once, so yesterday. Now i help people on discord. But this is a real problem, I know the code is coming from a good place so what better time. Deciding to apple to more human sigh, let's see how this social xp dodes;
Symbol.tsx
import type {
SkFont,
Vector,
SkiaValue,
SkiaClockValue,
} from "#shopify/react-native-skia";
import {
interpolate,
dist,
useComputedValue,
vec,
Group,
Text,
} from "#shopify/react-native-skia";
import React from "react";
import SimplexNoise from "simplex-noise";
import { useWindowDimensions } from "react-native";
import { FG } from "./Theme";
export const COLS = 5;
export const ROWS = 10;
const DIGITS = new Array(10).fill(0).map((_, i) => `${i}`);
const F = 0.0008;
const R = 125;
const A = 10;
interface SymbolProps {
i: number;
j: number;
font: SkFont;
pointer: SkiaValue<Vector>;
clock: SkiaClockValue;
}
export const Symbol = ({ i, j, font, pointer, clock }: SymbolProps) => {
const { width, height } = useWindowDimensions();
const SIZE = { width: width / COLS, height: height / ROWS };
const x = i * SIZE.width;
const y = j * SIZE.height;
const noise = new SimplexNoise(`${i}-${j}`);
const text = DIGITS[Math.round(Math.random() * 9)];
const [symbolWidth] = font.getGlyphWidths(font.getGlyphIDs(text));
const origin = vec(x + SIZE.width / 2, y + SIZE.height / 2);
const transform = useComputedValue(
() => [
{
scale: interpolate(
dist(pointer.current, origin),
[0, R],
[1.25, 0.25],
{
extrapolateLeft: "clamp",
extrapolateRight: "clamp",
}
),
},
],
[pointer]
);
const dx = useComputedValue(() => {
const d = A * noise.noise2D(x, clock.current * F);
return origin.x - symbolWidth / 2 + d;
}, [clock]);
const dy = useComputedValue(() => {
const d = A * noise.noise2D(y, clock.current * F);
return origin.y + font.getSize() / 2 + d;
}, [clock]);
return (
<Group transform={transform} origin={origin}>
<Text text={text} x={dx} y={dy} font={font} color={FG} />
</Group>
);
};
Severance.tsx
import {
Canvas,
Fill,
Group,
useClockValue,
useFont,
useTouchHandler,
useValue,
} from "#shopify/react-native-skia";
import React from "react";
import { useWindowDimensions } from "react-native";
import { CRT } from "./CRT";
import { COLS, ROWS, Symbol } from "./Symbol";
import { BG } from "./Theme";
const rows = new Array(COLS).fill(0).map((_, i) => i);
const cols = new Array(ROWS).fill(0).map((_, i) => i);
export const Severance = () => {
const { width, height } = useWindowDimensions();
const clock = useClockValue();
const font = useFont(require("./SF-Mono-Medium.otf"), height / ROWS);
const pointer = useValue({ x: width / 2, y: height / 2 });
const onTouch = useTouchHandler({
onActive: (pt) => {
pointer.current = pt;
},
});
if (font === null) {
return null;
}
return (
<Canvas style={{ flex: 1 }} onTouch={onTouch} debug>
<CRT>
<Group>
<Fill color={BG} />
{rows.map((_i, i) =>
cols.map((_j, j) => {
return (
<Symbol
key={`${i}-${j}`}
i={i}
j={j}
font={font}
pointer={pointer}
clock={clock}
/>
);
})
)}
</Group>
</CRT>
</Canvas>
);
};
CRT.tsx
import {
Group,
Skia,
RuntimeShader,
usePaintRef,
Paint,
vec,
} from "#shopify/react-native-skia";
import type { ReactNode } from "react";
import React from "react";
import { useWindowDimensions } from "react-native";
const source = Skia.RuntimeEffect.Make(`
uniform shader image;
uniform vec2 resolution;
vec2 curve(vec2 uv)
{
// as we near the edge of our screen apply greater distortion using a sinusoid.
float curvature = 6.0;
uv = uv * 2.0 - 1.0;
vec2 offset = abs(uv.yx) / curvature;
uv = uv + uv * offset * offset;
uv = uv * 0.5 + 0.5;
return uv;
}
vec4 scanLine(float x)
{
float f = 900;
float opacity = 0.25;
float intensity = ((0.5 * sin(x * f)) + 0.5) * 0.9 + 0.1;
return vec4(vec3(pow(intensity, opacity)), 1.0);
}
half4 main(float2 xy) {
vec2 uv = xy/resolution;
vec2 curvedUV = curve(vec2(uv.x, uv.y));
vec4 baseColor = image.eval(curvedUV * resolution);
baseColor *= scanLine(curvedUV.x);
baseColor *= scanLine(curvedUV.y);
baseColor *= vec4(vec3(1.5), 1.0);
if (curvedUV.x < 0.0 || curvedUV.y < 0.0 || curvedUV.x > 1.0 || curvedUV.y > 1.0){
return vec4(0.0, 0.0, 0.0, 1.0);
} else {
return baseColor;
}
}
`)!;
interface CRTProps {
children: ReactNode | ReactNode[];
}
export const CRT = ({ children }: CRTProps) => {
const paint = usePaintRef();
const { width, height } = useWindowDimensions();
return (
<>
<Paint ref={paint}>
<RuntimeShader
source={source}
uniforms={{ resolution: vec(width, height) }}
/>
</Paint>
<Group layer={paint}>{children}</Group>
</>
);
};
index.ts
export * from "./Severance";
Issue occurs in Symbol.tsx at const noise = SimplexNoise declaration, please help.
It's been an absolute pleasure ladies, hardly knew thee.
I guess it’s pretty niche software. It was predictably version 4.0 updates, however I was trying to format with seeding using a now separated alea feature when I just had to format for randomization now all works perfectly. Wallah folks

Change the opacity outside of a cropping rectangle in konva

I'm trying to build a image cropping tool with React and konva. I'd like to change the opacity outside of the cropping rectangle to blur the rest of the image.
I have so far tried to set different opacities to the rectangle and the image but failed. I have looked up and there is no direct way to doing this
Here's the cropping function that I adapted to react with the help of this answer
import React, { useState, useEffect, useRef } from "react";
import { render } from "react-dom";
import { Stage, Layer, Rect, Image } from "react-konva";
import Konva from "konva";
const App = () => {
// Stage dims
let sW = 720,
sH = 720,
sX = 0,
sY = 0;
let src = "https://dummyimage.com/720x720/e85de8/fff&text=SO Rocks!";
let img = document.createElement("img");
useEffect(() => {
img.src = src;
function loadStatus() {
setloading(false);
}
img.addEventListener("load", loadStatus);
return () => {
img.removeEventListener("load", loadStatus);
};
}, [img, src]);
let scale = 1;
const [loading, setloading] = useState(true);
const [posStart, setposStart] = useState({});
const [posNow, setposNow] = useState({});
const [mode, setmode] = useState("");
/**
* Sets the state of posStart and posNow for tracking the coordinates of the cropping rectangle
* #param {Object} posIn - Coordinates of the pointer when MouseDown is fired
*/
function startDrag(posIn) {
setposStart({ x: posIn.x, y: posIn.y });
setposNow({ x: posIn.x, y: posIn.y });
}
/**
* Updates the state accordingly when the MouseMove event is fired
* #param {Object} posIn - Coordiantes of the current position of the pointer
*/
function updateDrag(posIn) {
setposNow({ x: posIn.x, y: posIn.y });
let posRect = reverse(posStart, posNow);
r2.current.x(posRect.x1);
r2.current.y(posRect.y1);
r2.current.width(posRect.x2 - posRect.x1);
r2.current.height(posRect.y2 - posRect.y1);
r2.current.visible(true);
}
/**
* Reverse coordinates if dragged left or up
* #param {Object} r1 - Coordinates of the starting position of cropping rectangle
* #param {Object} r2 - Coordinates of the current position of cropping rectangle
*/
function reverse(r1, r2) {
let r1x = r1.x,
r1y = r1.y,
r2x = r2.x,
r2y = r2.y,
d;
if (r1x > r2x) {
d = Math.abs(r1x - r2x);
r1x = r2x;
r2x = r1x + d;
}
if (r1y > r2y) {
d = Math.abs(r1y - r2y);
r1y = r2y;
r2y = r1y + d;
}
return { x1: r1x, y1: r1y, x2: r2x, y2: r2y }; // return the corrected rect.
}
/**
* Crops the image and saves it in jpeg format
* #param {Konva.Rect} r - Ref of the cropping rectangle
*/
function setCrop(r) {
let jpeg = new Konva.Image({
image: img,
x: sX,
y: sY
});
jpeg.cropX(r.x());
jpeg.cropY(r.y());
jpeg.cropWidth(r.width() * scale);
jpeg.cropHeight(r.height() * scale);
jpeg.width(r.width());
jpeg.height(r.height());
const url = jpeg.toDataURL({ mimeType: "image/jpeg", quality: "1.0" });
const a = document.createElement("a");
a.href = url;
a.download = "cropped.jpg";
a.click();
}
// Foreground rect to capture events
const r1 = useRef();
// Cropping rect
const r2 = useRef();
const image = useRef();
return (
<div className="container">
<Stage width={sW} height={sH}>
<Layer>
{!loading && (
<Image
ref={image}
{...{
image: img,
x: sX,
y: sY
}}
/>
)}
<Rect
ref={r1}
{...{
x: 0,
y: 0,
width: sW,
height: sH,
fill: "white",
opacity: 0
}}
onMouseDown={function (e) {
setmode("drawing");
startDrag({ x: e.evt.layerX, y: e.evt.layerY });
}}
onMouseMove={function (e) {
if (mode === "drawing") {
updateDrag({ x: e.evt.layerX, y: e.evt.layerY });
image.current.opacity(0.5);
r2.current.opacity(1);
}
}}
onMouseUp={function (e) {
setmode("");
r2.current.visible(false);
setCrop(r2.current);
image.current.opacity(1);
}}
/>
<Rect
ref={r2}
listening={false}
{...{
x: 0,
y: 0,
width: 0,
height: 0,
stroke: "white",
dash: [5, 5]
}}
/>
</Layer>
</Stage>
</div>
);
};
render(<App />, document.getElementById("root"));
Demo for the above code
It can be done using Konva.Group and its clip property. Add a new Konva.Image to the group and set its clipping positions and size to be the same as the cropping rectangle. Don't forget to set the listening prop of the group to false otherwise it will complicate things. Here's the final result
import { render } from "react-dom";
import React, { useLayoutEffect, useRef, useState } from "react";
import { Stage, Layer, Image, Rect, Group } from "react-konva";
/**
* Crops a portion of image in Konva stage and saves it in jpeg format
* #param {*} props - Takes no props
*/
function Cropper(props) {
// Stage dims
let sW = 720,
sH = 720,
sX = 0,
sY = 0;
let src = "https://dummyimage.com/720x720/e85de8/fff&text=SO Rocks!";
let img = new window.Image();
useLayoutEffect(() => {
img.src = src;
function loadStatus() {
// img.crossOrigin = "Anonymous";
setloading(false);
}
img.addEventListener("load", loadStatus);
return () => {
img.removeEventListener("load", loadStatus);
};
}, [img, src]);
let i = new Konva.Image({
x: 0,
y: 0,
width: 0,
height: 0
});
let scale = 1;
const [loading, setloading] = useState(true);
const [posStart, setposStart] = useState({});
const [posNow, setposNow] = useState({});
const [mode, setmode] = useState("");
/**
* Sets the state of posStart and posNow for tracking the coordinates of the cropping rectangle
* #param {Object} posIn - Coordinates of the pointer when MouseDown is fired
*/
function startDrag(posIn) {
setposStart({ x: posIn.x, y: posIn.y });
setposNow({ x: posIn.x, y: posIn.y });
}
/**
* Updates the state accordingly when the MouseMove event is fired
* #param {Object} posIn - Coordiantes of the current position of the pointer
*/
function updateDrag(posIn) {
setposNow({ x: posIn.x, y: posIn.y });
let posRect = reverse(posStart, posNow);
r2.current.x(posRect.x1);
r2.current.y(posRect.y1);
r2.current.width(posRect.x2 - posRect.x1);
r2.current.height(posRect.y2 - posRect.y1);
r2.current.visible(true);
grp.current.clip({
x: posRect.x1,
y: posRect.y1,
width: posRect.x2 - posRect.x1,
height: posRect.y2 - posRect.y1
});
grp.current.add(i);
i.image(img);
i.width(img.width);
i.height(img.height);
i.opacity(1);
}
/**
* Reverse coordinates if dragged left or up
* #param {Object} r1 - Coordinates of the starting position of cropping rectangle
* #param {Object} r2 - Coordinates of the current position of cropping rectangle
*/
function reverse(r1, r2) {
let r1x = r1.x,
r1y = r1.y,
r2x = r2.x,
r2y = r2.y,
d;
if (r1x > r2x) {
d = Math.abs(r1x - r2x);
r1x = r2x;
r2x = r1x + d;
}
if (r1y > r2y) {
d = Math.abs(r1y - r2y);
r1y = r2y;
r2y = r1y + d;
}
return { x1: r1x, y1: r1y, x2: r2x, y2: r2y }; // return the corrected rect.
}
/**
* Crops the image and saves it in jpeg format
* #param {Konva.Rect} r - Ref of the cropping rectangle
*/
function setCrop(r) {
let jpeg = new Konva.Image({
image: img,
x: sX,
y: sY
});
jpeg.cropX(r.x());
jpeg.cropY(r.y());
jpeg.cropWidth(r.width() * scale);
jpeg.cropHeight(r.height() * scale);
jpeg.width(r.width());
jpeg.height(r.height());
const url = jpeg.toDataURL({ mimeType: "image/jpeg", quality: "1.0" });
const a = document.createElement("a");
a.href = url;
a.download = "cropped.jpg";
a.click();
}
// Foreground rect to capture events
const r1 = useRef();
// Cropping rect
const r2 = useRef();
const image = useRef();
const grp = useRef();
return (
<div className="container">
<Stage width={sW} height={sH}>
<Layer>
{!loading && (
<Image
ref={image}
listening={false}
{...{
image: img,
x: sX,
y: sY
}}
/>
)}
<Rect
ref={r1}
{...{
x: 0,
y: 0,
width: sW,
height: sH,
fill: "white",
opacity: 0
}}
onMouseDown={function (e) {
setmode("drawing");
startDrag({ x: e.evt.layerX, y: e.evt.layerY });
image.current.opacity(0.2);
}}
onMouseMove={function (e) {
if (mode === "drawing") {
updateDrag({ x: e.evt.layerX, y: e.evt.layerY });
}
}}
onMouseUp={function (e) {
setmode("");
r2.current.visible(false);
setCrop(r2.current);
image.current.opacity(1);
grp.current.removeChildren(i);
}}
/>
<Group listening={false} ref={grp}></Group>
<Rect
ref={r2}
listening={false}
{...{
x: 0,
y: 0,
width: 0,
height: 0,
stroke: "white",
dash: [5, 10]
}}
/>
</Layer>
</Stage>
</div>
);
}
render(<Cropper />, document.getElementById("root"));
Thanks to #Vanquished Wombat for all the precious inputs. This is an adaptation of his answer here
Demo of the above code

ChartJS - Doughnut Segment Custom Size

I'm building a React component that generates a speedometer and I want to set the length of each segment (i.e. Red - 30%, Yellow - 30%, Green - 30%, and Gray - 10%).
I'm using React-ChartJS-2 and onComplete of the animation I'm drawing the text and needle.
I've checked the documentation and there's nothing for setting the length or width of each segment. In an ideal situation, the needle would be in the green... and yes, our numbers allow for 100+%
Anyone have an idea on how to do this, either a plug-in or a callback function that I can tie into that allows me to customize how each segment is drawn?
<Box>
<Box style={{ position: 'relative', paddingLeft: theme.spacing(1.25), height: 300 }}>
<Doughnut
ref={chartRef}
data={{
labels: labels ?? data,
datasets: [
{
data: data,
// Use backgroundColor if available or generate colors based on number of data points
backgroundColor: backgroundColor ?? generateColorArray(20, 360, data.length),
borderWidth: 0
}
]
}}
options={{
legend: {
display: false,
position: 'top',
fullWidth: true
},
layout: {
padding: {
top: 50,
left: 25,
right: 25,
bottom: 25
}
},
rotation: 1 * Math.PI,
circumference: Math.PI,
cutoutPercentage: 70,
animation: {
duration: 500,
easing: 'easeOutQuart',
onComplete: function(e): void {
drawNeedle(e.chart.innerRadius, e.chart.outerRadius);
drawText(`${needleValue.toString()}%`, e.chart.innerRadius, e.chart.outerRadius);
},
onProgress: function(e): void {
console.log('e: ', e);
}
},
tooltips: {
enabled: false
},
// Disable other events from firing which causes the chart elements to get pushed down onHover
events: []
}}
/>
</Box>
</Box>
const drawNeedle = (innerRadius: number, outerRadius: number): void => {
const chart = chartRef.current as Doughnut;
const ctx = chart.chartInstance.ctx;
const maxValue = 180;
if (ctx) {
const rotation = -Math.PI;
const circumference = Math.PI;
const origin = rotation + circumference * (0 / maxValue);
const target = rotation + circumference * (((needleValue / 100) * 180) / maxValue);
const angle = origin + (target - origin) * 1;
const chartArea = chart.chartInstance.chartArea;
const width = chartArea.right - chartArea.left;
const needleRadius = (2 / 100) * width;
const needleWidth = (3.2 / 100) * width;
const needleLength = (20 / 100) * (outerRadius - innerRadius) + innerRadius;
const cw = ctx.canvas.offsetWidth;
const ch = ctx.canvas.offsetHeight;
const cx = cw / 2;
const cy = ch - ch / 14;
ctx.save();
ctx.translate(cx, cy);
ctx.rotate(angle);
ctx.fillStyle = 'rgba(0, 0, 0, 1)';
// draw circle
ctx.beginPath();
ctx.ellipse(0, 0, needleRadius, needleRadius, 0, 0, 2 * Math.PI);
ctx.fill();
// draw needle
ctx.beginPath();
ctx.moveTo(0, needleWidth / 2);
ctx.lineTo(needleLength, 0);
ctx.lineTo(0, -needleWidth / 2);
ctx.fill();
ctx.restore();
}
};
const drawText = (text: string, innerRadius: number, outerRadius: number): void => {
const chart = chartRef.current as Doughnut;
const ctx = chart.chartInstance.ctx;
const minValue = Math.min(...data);
const maxValue = Math.max(...data);
if (ctx) {
ctx.fillStyle = 'rgba(0, 0, 0, 1)';
const chartArea = chart.chartInstance.chartArea;
const centerX = (chartArea.left + chartArea.right) / 2;
const centerY = (chartArea.top + chartArea.bottom) / 2;
const textMetrics = ctx.measureText(text);
const textHeight = Math.max(ctx.measureText('m').width, ctx.measureText('\uFF37').width);
const radialDiff = outerRadius - innerRadius;
// Min / Max values
ctx.font = '20px Arial';
ctx.fillText(`${minValue}%`, chartArea.left + radialDiff * 1.1, chartArea.bottom + textHeight * 2);
ctx.fillText(`${maxValue}%`, chartArea.right - radialDiff * 2, chartArea.bottom + textHeight * 2);
// Needle value
ctx.font = '30px Arial';
ctx.fillText(text, centerX - textMetrics.width, centerY + textHeight);
}
};

Resources