我想使用 ReactJS 开发一个设计,其中每一侧的值之和将为 100%,并且值将分为 3 个可以动态的部分
中间部分应按照图中所示的值进行维护。
这是我的代码
import React from 'react';
import styled from 'styled-components';
export const CONTAINER_HEIGHT = 450;
export const ParentDiv = styled.div`
display: flex;
`;
export const OverLay = styled.div`
width: 0;
height: 0;
border-style: solid;
position: absolute;
bottom: 0;
`;
export const PercentageDivContainer = styled.div`
height: ${(): string => `${CONTAINER_HEIGHT}px`};
`;
export const PercentageDivContainerMID = styled.div`
width: 140px;
`;
export const PercentageDiv = styled.div`
width: 70px;
display: grid;
place-content: center;
`;
export const hexToRgbA = (hex: string, opacity: number = 1): string => {
let color: any;
const pattern = /^#([A-Fa-f0-9]{3}){1,2}$/.test(hex);
if (pattern) {
color = hex.substring(1).split('');
if (color.length == 3) {
color = [color[0], color[0], color[1], color[1], color[2], color[2]];
}
color = '0x' + color.join('');
return 'rgba(' + [(color >> 16) & 255, (color >> 8) & 255, color & 255].join(',') + ',' + opacity + ')';
}
throw new Error('Bad Hex');
};
const createChartComponent: React.FC = () => {
const borderTrangle = {
// http://apps.eky.hk/css-triangle-generator/
topToLeft: {
borderWidth: '-x- 140px 0 0',
borderColor: '-x- transparent transparent transparent',
},
topToRight: {
borderWidth: '0 140px -x- 0',
borderColor: 'transparent -x- transparent transparent',
},
bottomToLeft: {
borderWidth: '-x- 0 0 140px',
borderColor: ' transparent transparent transparent -x-',
},
bottomToRight: {
borderWidth: '0 0 -x- 140px',
borderColor: 'transparent transparent -x- transparent',
},
};
const data = {
users: [
{
p: 40,
},
{
p: 40,
},
{
p: 20,
},
],
usage: [
{
p: 35.7,
},
{
p: 35.2,
},
{
p: 29.1,
},
],
};
const { users, usage } = data;
const getTriangleHeight = (leftPercentage, rightPercentage) => {
const maxHeightForMiddle = Math.max(...[leftPercentage, rightPercentage]);
// Calculate the heights based on percentages and the container width
const leftHeight = (leftPercentage * CONTAINER_HEIGHT) / 100;
const rightHeight = (rightPercentage * CONTAINER_HEIGHT) / 100;
// Calculate the triangle height by subtracting rightHeight from leftHeight
const triangleHeight = Math.abs(leftHeight - rightHeight);
return {
triangleHeight,
maxHeightForMiddle,
isLeftValueGreater: leftPercentage < rightPercentage,
};
};
const createBorderCurveFunc = (data) => {
const { users, usage } = data;
if (users.length !== usage.length) return;
const { topToRight, topToLeft, bottomToLeft, bottomToRight } = borderTrangle;
return Array.from({ length: users.length }).map((_, index) => {
let currentTrangle = getTriangleHeight(users[index]?.p, usage[index]?.p);
let isPositiveValue = currentTrangle.isLeftValueGreater ? [topToRight, bottomToLeft] : [topToLeft, bottomToRight];
return (
<div
style={{
height: `${currentTrangle.maxHeightForMiddle}%`,
position: 'relative',
}}
>
<>
<OverLay
style={{
borderColor: isPositiveValue[0].borderColor.replace('-x-', `blue`),
borderWidth: isPositiveValue[0].borderWidth.replace('-x-', `${currentTrangle.triangleHeight}px`),
}}
></OverLay>
<OverLay
style={{
borderColor: isPositiveValue[1].borderColor.replace('-x-', `red`),
borderWidth: isPositiveValue[1].borderWidth.replace('-x-', `${currentTrangle.triangleHeight}px`),
}}
></OverLay>
</>
</div>
);
// }
});
};
const getColor = (color, index) => hexToRgbA(color, index === 0 ? 1 : 1 - index * 0.33);
return (
<ParentDiv>
<PercentageDivContainer>
{users.map((item, index) => (
<PercentageDiv style={{ height: `${item.p}%`, backgroundColor: getColor('#007bff', index) }}>{item.p}</PercentageDiv>
))}
</PercentageDivContainer>
<PercentageDivContainerMID>{createBorderCurveFunc(data)}</PercentageDivContainerMID>
<PercentageDivContainer>
{usage.map((item, index) => (
<PercentageDiv style={{ height: `${item.p}%`, backgroundColor: getColor('#007bff', index) }}>{item.p}</PercentageDiv>
))}
</PercentageDivContainer>
</ParentDiv>
);
};
export default createChartComponent;
我目前正在努力创建一个边框三角形。 为了首先制作中间截面形状,我比较左右百分比之间的最大高度,在我们的例子中为 81.1%。因为它是一个重要的值,所以我将中间元素的高度设置为 81.1%,并在其中放置 2 个 div 作为绝对值,在其上创建边框形状。然后,我计算必要的差异来创建边框三角形以匹配边缘并将其绝对定位在底部。对于第一次迭代,如果左 > 右,右 > 左,这两种情况的结果都是完美的。然而, 我在确定如何定位第二次和第三次放置的元素和边框三角形时面临着挑战。这是代码的输出。
您当前的代码不使用
d3.js
,但我假设您愿意接受这样的解决方案,因为您用它标记了问题。我认为尝试使用 html 和 div 创建这样的可视化是不适合这项工作的工具。无论如何,这是我用 d3 拍摄的照片。
对我来说,你的模型看起来就像一个精美的堆叠条形图,所以我创建了这样的图表。困难的部分实际上是所有自定义标签。我在下面发表了评论,希望它是不言自明的:
<!DOCTYPE html>
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.5/d3.js"></script>
</head>
<body>
<div id="chart"></div>
<script>
const margin = { top: 10, right: 10, bottom: 20, left: 20 },
width = 460 - margin.left - margin.right,
height = 400 - margin.top - margin.bottom;
const svg = d3
.select('#chart')
.append('svg')
.attr('width', width + margin.left + margin.right)
.attr('height', height + margin.top + margin.bottom)
.append('g')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')');
const data = {
users: [
{
p: 40,
},
{
p: 40,
},
{
p: 20,
},
],
usage: [
{
p: 35.7,
},
{
p: 35.2,
},
{
p: 29.1,
},
],
};
// in order to use d3.stack, we need the data in the format of...
// [ {p1: 40, p2: 40, p3: 20}, {p1: 35.7, p2: 35.2, p3: 29.1} ]
const stackableData = [
data.users.reduce((a, d, i) => {
a[i] = d.p;
return a;
}, {}),
data.usage.reduce((a, d, i) => {
a[i] = d.p;
return a;
}, {}),
];
// let d3 do the stack magic. This essentially calculates rolling sums at each array position
const keys = Object.keys(stackableData[0]);
const stackedData = d3.stack().keys(keys)(stackableData);
// set up our scales. For the x we are just using two columns
// adjust padding to make the "flow" part wider/narrower
const x = d3.scaleBand().domain([0, 1]).range([0, width]).padding([0.5]);
// y scale assumes both stacks are 100%
const y = d3.scaleLinear().domain([0, 100]).range([height, 0]);
// colors...
const color = d3
.scaleOrdinal()
.domain(keys)
.range(['#5E7AC5', '#9AACDB', '#3456B1']);
// draw the 6 rectangles of our stacks
svg
.selectAll(null)
.data(stackedData)
.enter()
.append('g')
.attr('class', 'pair')
.attr('fill', (d) => color(d.key))
.selectAll('rect')
.data((d) => d)
.enter()
.append('rect')
.attr('x', (d, i) => x(i))
.attr('y', (d) => y(d[1]))
.attr('height', (d) => y(d[0]) - y(d[1]))
.attr('width', x.bandwidth());
// draw the "flow" between the stacks
d3.selectAll('.pair')
.append('polygon')
.attr('points', (d, i) => {
let r1 = d[0],
r2 = d[1],
rv = '';
rv += x(0) + x.bandwidth() + ',' + y(r1[0]) + ' ';
rv += x(0) + x.bandwidth() + ',' + y(r1[1]) + ' ';
rv += x(1) + ',' + y(r2[1]) + ' ';
rv += x(1) + ',' + y(r2[0]) + ' ';
return rv;
})
.attr('fill-opacity', 0.8);
// hacky part here, add all the custom labels
// since the "y-axis" is in the same y position as the percent, hack that in
[0, 0, 1].forEach((j, k) => {
d3.selectAll('.pair')
.append('text')
.text((d, i) =>
k == 0 ? ['Light', 'Medium', 'Heavy'][i] : d[j].data[i] + '%'
)
.attr('font-size', '14')
.attr('font-family', 'sans-serif')
.attr('text-anchor', 'middle')
.attr('x', (d, i) =>
k == 0 ? margin.left : x(j) + x.bandwidth() / 2
)
.attr('y', (d, i) => {
let r = d[j];
return (y(r[0]) - y(r[1])) / 2 + y(r[1]);
})
.attr('dy', '0.71em')
.attr('fill', k == 0 ? 'black' : 'white');
});
// "x-axis"
svg
.selectAll(null)
.data(['Users', 'Usage'])
.enter()
.append('text')
.text((d) => d)
.attr('x', (d, i) => x(i) + x.bandwidth() / 2)
.attr('font-size', '14')
.attr('font-family', 'sans-serif')
.attr('text-anchor', 'middle')
.attr('y', height)
.attr('dy', '1.1em');
</script>
</body>
</html>