react-charting
Version:
Chart components for ReactJS
1,188 lines (1,081 loc) • 36.9 kB
JSX
import React from "react"
import ReactDOM from "react-dom"
import interact from "interact.js"
function defaultLabelFunc(value) {
return `${value}`
}
export default class XYDiagram extends React.PureComponent {
static displayName = "XYDiagram"
static propTypes = {
width: React.PropTypes.number.isRequired,
height: React.PropTypes.number.isRequired,
labelFont: React.PropTypes.string,
labelSize: React.PropTypes.number,
labelColor: React.PropTypes.string,
centerText: React.PropTypes.string,
centerTextFont: React.PropTypes.string,
centerTextSize: React.PropTypes.number,
centerTextColor: React.PropTypes.string,
axisColor: React.PropTypes.string,
backgroundColor: React.PropTypes.string,
leftAxisXOffset: React.PropTypes.number,
rightAxisXOffset: React.PropTypes.number,
x: React.PropTypes.array.isRequired,
xStep: React.PropTypes.number,
xMin: React.PropTypes.oneOfType([React.PropTypes.number, React.PropTypes.func]),
xMax: React.PropTypes.oneOfType([React.PropTypes.number, React.PropTypes.func]),
xFactor: React.PropTypes.number,
xGridOrigin: React.PropTypes.number,
xGridSpacing: React.PropTypes.number,
xGridAtPoint: React.PropTypes.bool,
xGridColor: React.PropTypes.string,
xLabelOrigin: React.PropTypes.number,
xLabelSpacing: React.PropTypes.number,
xLabelAtPoint: React.PropTypes.bool,
xLabelFunc: React.PropTypes.func,
xLabelNoOverlapCheck: React.PropTypes.bool, // overlap checks only work from left to right, disable if points are right to left
y: React.PropTypes.array.isRequired,
yMin: React.PropTypes.number,
yMax: React.PropTypes.number,
yFactor: React.PropTypes.number,
yValuesPerX: React.PropTypes.number, // show that number of bars per point, y.length == x.length * yValuesPerX
yColor: React.PropTypes.string,
yOpacity: React.PropTypes.number,
yLabelColor: React.PropTypes.string,
yGridOrigin: React.PropTypes.number,
yGridSpacing: React.PropTypes.number,
yGridColor: React.PropTypes.string,
yLabelOrigin: React.PropTypes.number,
yLabelSpacing: React.PropTypes.number,
yLabelFunc: React.PropTypes.func,
yLabelAtPoint: React.PropTypes.bool,
yLineWidth: React.PropTypes.number,
yLineDashes: React.PropTypes.string,
markerSize: React.PropTypes.number,
yBars: React.PropTypes.number,
yAxisTitle: React.PropTypes.string,
y2: React.PropTypes.array,
y2Min: React.PropTypes.number,
y2Max: React.PropTypes.number,
y2Factor: React.PropTypes.number,
y2ValuesPerX: React.PropTypes.number, // show that number of bars per point, y2.length == x.length * y2ValuesPerX
y2Color: React.PropTypes.string,
y2Opacity: React.PropTypes.number,
y2LabelColor: React.PropTypes.string,
y2LabelOrigin: React.PropTypes.number,
y2LabelSpacing: React.PropTypes.number,
y2LabelFunc: React.PropTypes.func,
y2LabelAtPoint: React.PropTypes.bool,
y2LineWidth: React.PropTypes.number,
y2LineDashes: React.PropTypes.string,
markerSize2: React.PropTypes.number,
y2OnRightAxis: React.PropTypes.bool,
y2Bars: React.PropTypes.number,
y2AxisTitle: React.PropTypes.string,
y3: React.PropTypes.array,
y3Min: React.PropTypes.number,
y3Max: React.PropTypes.number,
y3Factor: React.PropTypes.number,
y3ValuesPerX: React.PropTypes.number, // show that number of bars per point, y3.length == x.length * y3ValuesPerX
y3Color: React.PropTypes.string,
y3Opacity: React.PropTypes.number,
y3LabelOrigin: React.PropTypes.number,
y3LabelSpacing: React.PropTypes.number,
y3LabelFunc: React.PropTypes.func,
y3LineWidth: React.PropTypes.number,
y3LineDashes: React.PropTypes.string,
markerSize3: React.PropTypes.number,
y3Bars: React.PropTypes.number,
y4: React.PropTypes.array,
y4Min: React.PropTypes.number,
y4Max: React.PropTypes.number,
y4Factor: React.PropTypes.number,
y4Color: React.PropTypes.string,
y4Opacity: React.PropTypes.number,
y4LabelOrigin: React.PropTypes.number,
y4LabelSpacing: React.PropTypes.number,
y4LabelFunc: React.PropTypes.func,
y4LineWidth: React.PropTypes.number,
y4LineDashes: React.PropTypes.string,
markerSize4: React.PropTypes.number,
bgValues: React.PropTypes.array,
bgColorFunc: React.PropTypes.func,
onClickMarker: React.PropTypes.func,
selectedIndex: React.PropTypes.number,
selectedMarkerSize: React.PropTypes.number,
selectedMarkerColor: React.PropTypes.string,
leftMarkerX: React.PropTypes.number,
leftMarkerColor: React.PropTypes.string,
leftMarkerDragged: React.PropTypes.func,
rightMarkerX: React.PropTypes.number,
rightMarkerColor: React.PropTypes.string,
rightMarkerDragged: React.PropTypes.func,
leftRightMarkerCoverColor: React.PropTypes.string,
leftRightMarkerCoverOpacity: React.PropTypes.number,
leftRightMarkerWidth: React.PropTypes.number,
extraPointsX: React.PropTypes.array,
extraPointsY: React.PropTypes.array,
extraPointsSize: React.PropTypes.number,
extraPointsColor: React.PropTypes.string,
}
static defaultProps = {
labelFont: `"Lucida Console", Monaco, monospace`,
labelSize: 16,
labelColor: "#000",
centerText: "",
centerTextFont: `"Lucida Console", Monaco, monospace`,
centerTextSize: 48,
centerTextColor: "#000",
axisColor: "#000",
backgroundColor: "#FFF",
xStep: 1,
xMin: 0,
xMax: 1,
xFactor: 1,
xGridOrigin: 0,
xGridAtPoint: false,
xGridColor: "#AAA",
xLabelOrigin: 0,
xLabelAtPoint: false,
xLabelFunc: defaultLabelFunc,
xLabelNoOverlapCheck: false,
yMin: 0,
yMax: 1,
yFactor: 1,
yValuesPerX: 1,
yColor: "#0082C8",
yOpacity: 1,
yGridOrigin: 0,
yGridColor: "#AAA",
yLabelOrigin: 0,
yLabelFunc: defaultLabelFunc,
yLabelAtPoint: false,
yLineWidth: 1,
markerSize: 0,
yBars: 0,
y2Min: 0,
y2Max: 1,
y2Factor: 1,
y2ValuesPerX: 1,
y2Color: "#92D050",
y2Opacity: 1,
y2LabelOrigin: 0,
y2LabelFunc: defaultLabelFunc,
y2LabelAtPoint: false,
y2LineWidth: 1,
markerSize2: 0,
y2OnRightAxis: false,
y2Bars: 0,
y3Min: 0,
y3Max: 1,
y3Factor: 1,
y3ValuesPerX: 1,
y3Color: "#FF00FF",
y3Opacity: 1,
y3LabelOrigin: 0,
y3LabelFunc: defaultLabelFunc,
y3LineWidth: 1,
markerSize3: 0,
y3Bars: 0,
y4Min: 0,
y4Max: 1,
y4Factor: 1,
y4Color: "#647280",
y4Opacity: 1,
y4LabelOrigin: 0,
y4LabelFunc: defaultLabelFunc,
y4LineWidth: 1,
markerSize4: 0,
selectedIndex: -1,
selectedMarkerSize: 0,
selectedMarkerColor: "#0082C8",
leftMarkerX: NaN,
leftMarkerColor: "#11F",
rightMarkerX: NaN,
rightMarkerColor: "#11F",
leftRightMarkerCoverColor: "#666",
leftRightMarkerCoverOpacity: 0.5,
leftRightMarkerWidth: 16,
extraPointsSize: 5,
extraPointsColor: "#F00",
}
static shiftArray(a, val) {
a.copyWithin(0, 1)
a[a.length - 1] = val
}
static rotateArray(a) {
XYDiagram.shiftArray(a, a[0])
}
static calcTextWidth(text, labelSize) {
return text.length * labelSize * 0.6
}
static calcLeftAxisXOffset(yMin, yMax, yAxisTitle = null, yLabelFunc = defaultLabelFunc, labelSize = XYDiagram.defaultProps.labelSize) {
const textHeight = labelSize * 1.2
const minLabel = yLabelFunc(yMin, yMin, yMax)
const maxLabel = yLabelFunc(yMax, yMin, yMax)
let textWidth = Math.max(XYDiagram.calcTextWidth(minLabel, labelSize), XYDiagram.calcTextWidth(maxLabel, labelSize))
if (yAxisTitle) {
textWidth = Math.max(textWidth, XYDiagram.calcTextWidth(yAxisTitle, labelSize) * 0.5)
}
return textHeight * 0.2 + textWidth
}
static calcRightAxisXOffset(y2Min, y2Max, y2AxisTitle = null, y2LabelFunc = defaultLabelFunc, labelSize = XYDiagram.defaultProps.labelSize) {
const textHeight = labelSize * 1.2
const minLabel = y2LabelFunc(y2Min, y2Min, y2Max)
const maxLabel = y2LabelFunc(y2Max, y2Min, y2Max)
let textWidth = Math.max(XYDiagram.calcTextWidth(minLabel, labelSize), XYDiagram.calcTextWidth(maxLabel, labelSize))
if (y2AxisTitle) {
textWidth = Math.max(textWidth, XYDiagram.calcTextWidth(y2AxisTitle, labelSize) * 0.5)
}
return textHeight * 0.2 + textWidth
}
constructor(...args) {
super(...args)
this.onClickMarker = this.onClickMarker.bind(this)
}
axisXOffset() {
if (this.props.leftAxisXOffset) {
return this.props.leftAxisXOffset
}
if (!this.hasLeftYLabels()) {
return 0
}
const { yMin, yMax, yAxisTitle, yLabelFunc, labelSize } = this.props
return XYDiagram.calcLeftAxisXOffset(yMin, yMax, yAxisTitle, yLabelFunc, labelSize)
}
rightAxisXOffset() {
if (this.props.rightAxisXOffset) {
return this.props.rightAxisXOffset
}
if (!this.hasRightYLabels()) {
return 0
}
const { y2Min, y2Max, y2AxisTitle, y2LabelFunc, labelSize } = this.props
return XYDiagram.calcRightAxisXOffset(y2Min, y2Max, y2AxisTitle, y2LabelFunc, labelSize)
}
onClickMarker(event) {
if (this.props.onClickMarker) {
const index = parseInt(event.target.getAttribute("data-index"))
const x = parseFloat(event.target.getAttribute("data-x"))
const y = parseFloat(event.target.getAttribute("data-y"))
const selected = event.target.hasAttribute("data-selected")
this.props.onClickMarker(index, x, y, selected)
}
}
makeDraggable(group, marker, onDragged) {
const parent = ReactDOM.findDOMNode(this.refs.parent)
group = ReactDOM.findDOMNode(group)
marker = ReactDOM.findDOMNode(marker)
interact(marker)
.draggable({
startAxis: "x",
lockAxis: "x",
preventDefault: "always",
restrict: {
restriction: parent,
elementRect: { top: 0, left: 0, bottom: 1, right: 1 },
}
})
.on("dragstart", (event) => {
event.preventDefault()
// Only way to change the class of an SVG element:
marker.setAttribute("class", "dragging")
})
.on("dragmove", (event) => {
const dx = event.clientX - event.clientX0
group.setAttribute("transform", `translate(${dx})`)
})
.on("dragend", (event) => {
// Only way to change the class of an SVG element:
marker.removeAttribute("class")
group.removeAttribute("transform")
if (onDragged) {
const clientX = event.clientX - parent.getBoundingClientRect().left
const { xFactor } = this.props
const xMin = this.xMin()
const xMax = this.xMax()
const leftMarkerVisible = this.isLeftMarkerVisible()
const xOffset = leftMarkerVisible ? this.props.leftRightMarkerWidth : 0
const axisXOffset = this.axisXOffset()
const rangeWidth = this.rangeWidth()
const xRange = xMax - xMin
const xScale = rangeWidth / xRange
// axisXOffset + xOffset + (x*xFactor - xMin) * xScale = clientX
// (x*xFactor - xMin) * xScale = clientX - axisXOffset - xOffset
// x*xFactor - xMin = (clientX - axisXOffset - xOffset) / xScale
// x*xFactor = (clientX - axisXOffset - xOffset) / xScale + xMin
const x = ((clientX - axisXOffset - xOffset) / xScale + xMin) / xFactor
onDragged(x, clientX)
}
})
}
componentDidMount() {
this.makeDraggable(this.refs.leftMarkerGroup, this.refs.leftMarker, this.props.leftMarkerDragged)
this.makeDraggable(this.refs.rightMarkerGroup, this.refs.rightMarker, this.props.rightMarkerDragged)
}
isLeftMarkerVisible() {
return !isNaN(this.props.leftMarkerX) && this.props.leftMarkerX !== this.props.rightMarkerX
}
isRightMarkerVisible() {
return !isNaN(this.props.rightMarkerX) && this.props.leftMarkerX !== this.props.rightMarkerX
}
hasXLabels() {
return !!this.props.xLabelSpacing || this.props.xLabelAtPoint
}
hasLeftYLabels() {
return !!this.props.yLabelSpacing && !this.props.y2OnRightAxis
}
hasRightYLabels() {
return !!this.props.y2LabelSpacing || (!!this.props.yLabelSpacing && this.props.y2OnRightAxis)
}
rangeWidth() {
let rangeWidth = this.props.width - this.axisXOffset() - this.rightAxisXOffset()
rangeWidth -= this.isLeftMarkerVisible() ? this.props.leftRightMarkerWidth : 0
rangeWidth -= this.isRightMarkerVisible() ? this.props.leftRightMarkerWidth : 0
return rangeWidth
}
xMin() {
const { xMin } = this.props
return (typeof xMin === "function") ? xMin(this.axisXOffset()) : xMin
}
xMax() {
const { xMax } = this.props
return (typeof xMax === "function") ? xMax(this.rightAxisXOffset()) : xMax
}
textWidth(text) {
return text.length * this.props.labelSize * 0.6
}
render() {
////////////////////////////////////////////////////////////////////////////////
// Set constants to work with:
const leftMarkerVisible = this.isLeftMarkerVisible()
const rightMarkerVisible = this.isRightMarkerVisible()
const { labelFont, labelSize, labelColor, xLabelOrigin, xLabelSpacing, xLabelAtPoint, yLabelAtPoint, xLabelFunc, xLabelNoOverlapCheck, xStep } = this.props
const { yLabelOrigin, yLabelSpacing, yLabelFunc, yColor, yOpacity, markerSize, yAxisTitle } = this.props
const { y2LabelOrigin, y2LabelSpacing, y2LabelFunc, y2LabelAtPoint, y2Color, y2Opacity, markerSize2, y2AxisTitle } = this.props
const { y3Color, y3Opacity, markerSize3 } = this.props
const { y4Color, y4Opacity, markerSize4 } = this.props
const yLabelColor = this.props.yLabelColor || yColor
const y2LabelColor = this.props.y2LabelColor || y2Color
const textHeight = labelSize * 1.2
const hasXLabels = this.hasXLabels()
const hasLeftYLabels = this.hasLeftYLabels()
const hasRightYLabels = this.hasRightYLabels()
const hasYLabels = hasLeftYLabels || hasRightYLabels
const { xFactor, width, height, selectedIndex } = this.props
const { yMin, yMax, yFactor, yBars } = this.props
const { y2Min, y2Max, y2Factor, y2Bars } = this.props
const { y3Min, y3Max, y3Factor, y3Bars } = this.props
const { y4Min, y4Max, y4Factor } = this.props
const { xGridOrigin, xGridSpacing, xGridAtPoint, xGridColor, yGridOrigin, yGridSpacing, yGridColor, backgroundColor } = this.props
const { yValuesPerX, y2ValuesPerX, y3ValuesPerX } = this.props
const xOffset = leftMarkerVisible ? this.props.leftRightMarkerWidth : 0
const axisXOffset = this.axisXOffset()
const axisXOffsetFixed = axisXOffset.toFixed()
const rightAxisXOffset = this.rightAxisXOffset()
const rightAxisX = width - rightAxisXOffset
const rightAxisXFixed = rightAxisX.toFixed(1)
const axisYOffset = hasXLabels ? textHeight : (hasYLabels ? textHeight * 0.5 : 0)
const axisYOffsetFixed = axisYOffset.toFixed(1)
const rangeWidth = this.rangeWidth()
let rangeTop = height - textHeight * 0.5
let rangeHeight = height - axisYOffset - textHeight * 0.5
if (yAxisTitle || y2AxisTitle) {
rangeTop -= textHeight
rangeHeight -= textHeight
}
const xMin = this.xMin()
const xMax = this.xMax()
const xRange = xMax - xMin
const yRange = yMax - yMin
const y2Range = y2Max - y2Min
const y3Range = y3Max - y3Min
const y4Range = y4Max - y4Min
const xScale = rangeWidth / xRange
const yScale = rangeHeight / yRange
const y2Scale = rangeHeight / y2Range
const y3Scale = rangeHeight / y3Range
const y4Scale = rangeHeight / y4Range
////////////////////////////////////////////////////////////////////////////////
// Transform x/y values to graphics coordinates:
function transformX(x) {
return axisXOffset + xOffset + (x*xFactor - xMin) * xScale
}
function transformY(y) {
return axisYOffset + (y*yFactor - yMin) * yScale
}
function transformY2(y2) {
return axisYOffset + (y2*y2Factor - y2Min) * y2Scale
}
function transformY3(y3) {
return axisYOffset + (y3*y3Factor - y3Min) * y3Scale
}
function transformY4(y4) {
return axisYOffset + (y4*y4Factor - y4Min) * y4Scale
}
////////////////////////////////////////////////////////////////////////////////
// Axis lines:
let axisPoints = `${axisXOffsetFixed},${rangeTop} ${axisXOffsetFixed},${axisYOffsetFixed} ${rightAxisX},${axisYOffsetFixed}`
if (hasRightYLabels) {
axisPoints += ` ${rightAxisXFixed},${rangeTop}`
}
////////////////////////////////////////////////////////////////////////////////
// Grid lines:
let xGridLinesD = ""
let yGridLinesD = ""
if (xGridAtPoint) {
for (const x of this.props.x) {
if (x >= xMin && x <= xMax) {
const tx = transformX(x).toFixed(1)
xGridLinesD += `M${tx} ${axisYOffsetFixed}V${rangeTop}`
}
}
} else if (xGridSpacing) {
const start = xGridOrigin !== 0 ? xGridOrigin : Math.ceil(xMin / xGridSpacing) * xGridSpacing
for (let x = start; x <= xMax; x += xGridSpacing) {
if (x >= xMin) {
const tx = transformX(x).toFixed(1)
xGridLinesD += `M${tx} ${axisYOffsetFixed}V${rangeTop}`
}
}
}
if (yGridSpacing) {
const start = yGridOrigin !== 0 ? yGridOrigin : Math.ceil(yMin / yGridSpacing) * yGridSpacing
for (let y = start; y <= yMax; y += yGridSpacing) {
if (y >= yMin) {
const ty = transformY(y).toFixed(1)
yGridLinesD += `M${axisXOffsetFixed} ${ty}H${rightAxisXFixed}`
}
}
}
let gridLines = null
if (xGridLinesD.length > 0 || yGridLinesD.length > 0) {
if (xGridColor === yGridColor) {
gridLines = <path d={xGridLinesD + yGridLinesD} stroke={xGridColor} strokeWidth="1"/>
} else {
gridLines = [
<path key="xGrid" d={xGridLinesD} stroke={xGridColor} strokeWidth="1"/>,
<path key="yGrid" d={yGridLinesD} stroke={yGridColor} strokeWidth="1"/>,
]
}
}
////////////////////////////////////////////////////////////////////////////////
// Labels:
const numYPoints = Math.min(this.props.x.length, this.props.y.length / yValuesPerX)
const labels = []
if (hasXLabels) {
let ticksD = ""
let lastTextRightX = -Number.MAX_VALUE
const addXLabel = x => {
if (x < xMin || x > xMax) {
return
}
const tx = transformX(x)
const txFixed = tx.toFixed(1)
ticksD += `M${txFixed} ${(axisYOffset - textHeight*0.2).toFixed(1)}V${axisYOffset}`
const text = xLabelFunc(x, xMin, xMax)
const halfWidth = this.textWidth(text) * 0.5
if (xLabelNoOverlapCheck || (tx - halfWidth > lastTextRightX)) {
lastTextRightX = tx + halfWidth
labels.push(
<text
key={"xlt" + x}
x={txFixed}
y={(-textHeight*0.1).toFixed(1)}
transform="scale(1,-1)"
textAnchor="middle"
fill={labelColor}
>
{text}
</text>
)
}
}
if (xLabelAtPoint) {
for (const x of this.props.x) {
addXLabel(x)
}
} else {
for (let x = xLabelOrigin !== 0 ? xLabelOrigin : Math.ceil(xMin / xLabelSpacing) * xLabelSpacing; x <= xMax; x += xLabelSpacing) {
addXLabel(x)
}
}
labels.push(<path key="xll" d={ticksD} stroke={labelColor} strokeWidth="1"/>)
}
if (yLabelAtPoint) {
const labelYOffset = labelSize * -0.5
for (let i = 0; i < numYPoints; i += xStep) {
const x = this.props.x[i]
const y = this.props.y[i * yValuesPerX]
if (!Number.isFinite(x) || !Number.isFinite(y) || x < xMin || x > xMax) {
continue
}
const tx = transformX(x)
const txFixed = tx.toFixed(1)
const ty = transformY(y)
const text = yLabelFunc(y, yMin, yMax)
labels.push(
<text
key={"ylap" + i}
x={txFixed}
y={(labelYOffset - ty).toFixed(1)}
transform="scale(1,-1)"
textAnchor="middle"
fill={yColor}
>
{text}
</text>
)
}
}
if (y2LabelAtPoint) {
const labelYOffset = labelSize * -0.5
for (let i = 0; i < numYPoints; i += xStep) {
const x = this.props.x[i]
const y2 = this.props.y2[i * y2ValuesPerX]
if (!Number.isFinite(x) || !Number.isFinite(y2) || x < xMin || x > xMax) {
continue
}
const tx = transformX(x)
const txFixed = tx.toFixed(1)
const ty2 = transformY2(y2)
const text = y2LabelFunc(y2, y2Min, y2Max)
labels.push(
<text
key={"y2lap" + i}
x={txFixed}
y={(labelYOffset - ty2).toFixed(1)}
transform="scale(1,-1)"
textAnchor="middle"
fill={y2Color}
>
{text}
</text>
)
}
}
if (hasLeftYLabels) {
let ticksD = ""
const tickX = (axisXOffset - textHeight * 0.2).toFixed(1)
const labelYOffset = labelSize * 0.3
const start = yLabelOrigin !== 0 ? yLabelOrigin : Math.ceil(yMin / yLabelSpacing) * yLabelSpacing
for (let y = start; y <= yMax; y += yLabelSpacing) {
if (y >= yMin) {
const ty = transformY(y)
ticksD += `M${tickX} ${ty.toFixed(1)}H${axisXOffsetFixed}`
labels.push(
<text
key={"ylt" + y}
x={tickX}
y={(labelYOffset - ty).toFixed(1)}
transform="scale(1,-1)"
textAnchor="end"
fill={yLabelColor}
>
{yLabelFunc(y, yMin, yMax)}
</text>
)
}
}
labels.push(<path key="yll" d={ticksD} stroke={yLabelColor} strokeWidth="1"/>)
}
if (hasRightYLabels) {
let ticksD = ""
const tickX = (rightAxisX + textHeight * 0.2).toFixed(1)
const labelYOffset = labelSize * 0.3
const start = y2LabelOrigin !== 0 ? y2LabelOrigin : Math.ceil(y2Min / y2LabelSpacing) * y2LabelSpacing
for (let y2 = start; y2 <= y2Max; y2 += y2LabelSpacing) {
if (y2 >= y2Min) {
const ty2 = transformY2(y2)
ticksD += `M${rightAxisXFixed} ${ty2.toFixed(1)}H${tickX}`
labels.push(
<text
key={"y2lt" + y2}
x={tickX}
y={(labelYOffset - ty2).toFixed(1)}
transform="scale(1,-1)"
fill={y2LabelColor}
>
{y2LabelFunc(y2, y2Min, y2Max)}
</text>
)
}
}
labels.push(<path key="y2ll" d={ticksD} stroke={y2LabelColor} strokeWidth="1"/>)
}
if (yAxisTitle) {
labels.push(
<text
key="yAxisTitle"
x={axisXOffset.toFixed(1)}
y={(-rangeTop - labelSize).toFixed(1)}
textAnchor="middle"
transform="scale(1,-1)"
fill={yColor}
>
{yAxisTitle}
</text>
)
}
if (y2AxisTitle) {
labels.push(
<text
key="y2AxisTitle"
x={rightAxisX.toFixed(1)}
y={(-rangeTop - labelSize).toFixed(1)}
textAnchor="middle"
transform="scale(1,-1)"
fill={y2LabelColor}
>
{y2AxisTitle}
</text>
)
}
////////////////////////////////////////////////////////////////////////////////
// y data series graphics:
const bars = []
let yPoints = ""
if (yBars) {
// draw y values as bars
const barWidth = yBars / yValuesPerX;
const yZero = transformY(0)
let moveLeft
if (!y2Bars && !y3Bars) {
moveLeft = yBars * 0.5
} else if (y2Bars && y3Bars) {
moveLeft = yBars + y2Bars * 0.5
} else {
moveLeft = yBars
}
for (let i = 0; i < numYPoints; i += xStep) {
for (let j = 0; j < yValuesPerX; j++) {
let x = this.props.x[i]
let y = this.props.y[i * yValuesPerX + j]
if (Number.isFinite(x) && Number.isFinite(y)) {
x = transformX(x) - moveLeft + (j * barWidth)
y = transformY(y)
let h = y - yZero
if (h < 0) {
h = -h
} else {
y = yZero
}
let key = "yBar" + i
if (j > 0) {
key += "_" + j
}
bars.push(<rect key={key} x={x.toFixed(1)} y={y.toFixed(1)} width={barWidth} height={h.toFixed(1)} stroke="none" fill={yColor} fillOpacity={yOpacity}/>)
}
}
}
} else {
// draw y values as line-strip
for (let i = 0; i < numYPoints; i += xStep) {
const x = this.props.x[i]
const y = this.props.y[i * yValuesPerX]
if (Number.isFinite(x) && Number.isFinite(y)) {
yPoints += `${transformX(x).toFixed(1)},${transformY(y).toFixed(1)} `
}
}
if (xStep > 1) {
// if we are stepping over values, make sure to add the last one
const i = numYPoints - 1
const x = this.props.x[i]
const y = this.props.y[i * yValuesPerX]
if (Number.isFinite(x) && Number.isFinite(y)) {
yPoints += `${transformX(x).toFixed(1)},${transformY(y).toFixed(1)} `
}
}
}
let selectedMarker = null
const hasSelectedMarker = !!this.props.selectedMarkerSize && selectedIndex >= 0 && selectedIndex < numYPoints
if (hasSelectedMarker) {
const i = selectedIndex
const x = this.props.x[i]
const y = this.props.y[i * yValuesPerX]
if (Number.isFinite(x) && Number.isFinite(y)) {
selectedMarker = (
<circle
cx={transformX(x).toFixed(1)}
cy={transformY(y).toFixed(1)}
r={this.props.selectedMarkerSize * 0.5}
stroke="none"
fill={this.props.selectedMarkerColor}
onClick={this.props.onClickMarker ? this.onClickMarker : null}
data-index={i}
data-x={x}
data-y={y}
data-selected
/>
)
}
}
////////////////////////////////////////////////////////////////////////////////
// y2 data series graphics:
let numY2Points = 0
let y2Points = ""
if (this.props.y2) {
numY2Points = Math.min(this.props.x.length, this.props.y2.length / y2ValuesPerX)
if (y2Bars) {
const barWidth = y2Bars / y2ValuesPerX;
const y2Zero = transformY2(0)
let moveLeft
if (yBars && !y3Bars) {
moveLeft = 0
} else if (!yBars && y3Bars) {
moveLeft = y2Bars
} else {
moveLeft = y2Bars * 0.5
}
for (let i = 0; i < numY2Points; i += xStep) {
for (let j = 0; j < y2ValuesPerX; j++) {
let x = this.props.x[i]
let y2 = this.props.y2[i * y2ValuesPerX + j]
if (Number.isFinite(x) && Number.isFinite(y2)) {
x = transformX(x) - moveLeft + (j * barWidth)
y2 = transformY2(y2)
let h = y2 - y2Zero
if (h < 0) {
h = -h
} else {
y2 = y2Zero
}
let key = "y2Bar" + i
if (j > 0) {
key += "_" + j
}
bars.push(<rect key={key} x={x.toFixed(1)} y={y2.toFixed(1)} width={barWidth} height={h.toFixed(1)} stroke="none" fill={y2Color} fillOpacity={y2Opacity}/>)
}
}
}
} else {
for (let i = 0; i < numY2Points; i += xStep) {
const x = this.props.x[i]
const y2 = this.props.y2[i * y2ValuesPerX]
if (Number.isFinite(x) && Number.isFinite(y2)) {
y2Points += ` ${transformX(x).toFixed(1)},${transformY2(y2).toFixed(1)}`
}
}
if (xStep > 1) {
const i = numY2Points - 1
const x = this.props.x[i]
const y2 = this.props.y2[i * y2ValuesPerX]
if (Number.isFinite(x) && Number.isFinite(y2)) {
y2Points += ` ${transformX(x).toFixed(1)},${transformY2(y2).toFixed(1)}`
}
}
}
}
////////////////////////////////////////////////////////////////////////////////
// y3 data series graphics:
let numY3Points = 0
let y3Points = ""
if (this.props.y3) {
numY3Points = Math.min(this.props.x.length, this.props.y3.length / y3ValuesPerX)
if (y3Bars) {
const barWidth = y3Bars / y3ValuesPerX;
const y3Zero = transformY3(0)
let moveLeft
if (!yBars && !y3Bars) {
moveLeft = y3Bars * 0.5
} else if (yBars && y3Bars) {
moveLeft = y2Bars * -0.5
} else {
moveLeft = 0
}
for (let i = 0; i < numY3Points; i += xStep) {
for (let j = 0; j < y3ValuesPerX; j++) {
let x = this.props.x[i]
let y3 = this.props.y3[i * y3ValuesPerX + j]
if (Number.isFinite(x) && Number.isFinite(y3)) {
x = transformX(x) - moveLeft + (j * barWidth)
y3 = transformY3(y3)
let h = y3 - y3Zero
if (h < 0) {
h = -h
} else {
y3 = y3Zero
}
let key = "y3Bar" + i
if (j > 0) {
key += "_" + j
}
bars.push(<rect key={key} x={x.toFixed(1)} y={y3.toFixed(1)} width={barWidth} height={h.toFixed(1)} stroke="none" fill={y3Color} fillOpacity={y3Opacity}/>)
}
}
}
} else {
for (let i = 0; i < numY3Points; i += xStep) {
const x = this.props.x[i]
const y3 = this.props.y3[i * y3ValuesPerX]
if (Number.isFinite(x) && Number.isFinite(y3)) {
y3Points += ` ${transformX(x).toFixed(1)},${transformY3(y3).toFixed(1)}`
}
}
if (xStep > 1) {
const i = numY3Points - 1
const x = this.props.x[i]
const y3 = this.props.y3[i * y3ValuesPerX]
if (Number.isFinite(x) && Number.isFinite(y3)) {
y3Points += ` ${transformX(x).toFixed(1)},${transformY3(y3).toFixed(1)}`
}
}
}
}
////////////////////////////////////////////////////////////////////////////////
// y4 data series graphics:
let numY4Points = 0
let y4Points = ""
if (this.props.y4) {
numY4Points = Math.min(this.props.x.length, this.props.y4.length)
for (let i = 0; i < numY4Points; i += xStep) {
const x = this.props.x[i]
const y4 = this.props.y4[i]
if (Number.isFinite(x) && Number.isFinite(y4)) {
y4Points += ` ${transformX(x).toFixed(1)},${transformY4(y4).toFixed(1)}`
}
}
if (xStep > 1) {
const i = numY4Points - 1
const x = this.props.x[i]
const y4 = this.props.y4[i]
if (Number.isFinite(x) && Number.isFinite(y4)) {
y4Points += ` ${transformX(x).toFixed(1)},${transformY4(y4).toFixed(1)}`
}
}
}
////////////////////////////////////////////////////////////////////////////////
// Markers for data series points:
const markers = []
if (markerSize > 0) {
const r = markerSize * 0.5
const onClick = this.props.onClickMarker ? this.onClickMarker : null
for (let i = 0; i < numYPoints; i += xStep) {
const x = this.props.x[i]
const y = this.props.y[i * yValuesPerX]
if (Number.isFinite(x) && Number.isFinite(y) && !(hasSelectedMarker && i === selectedIndex)) {
markers.push(
<circle
key={"marker_" + i}
cx={transformX(x).toFixed(1)}
cy={transformY(y).toFixed(1)}
r={r}
stroke="none"
fill={yColor}
onClick={onClick}
data-index={i}
data-x={x}
data-y={y}
/>
)
}
}
}
if (markerSize2 > 0) {
const r = markerSize2 * 0.5
for (let i = 0; i < numY2Points; i += xStep) {
const x = this.props.x[i]
const y = this.props.y2[i * y2ValuesPerX]
if (Number.isFinite(x) && Number.isFinite(y)) {
markers.push(
<circle
key={"marker2_" + i}
cx={transformX(x).toFixed(1)}
cy={transformY2(y).toFixed(1)}
r={r}
stroke="none"
fill={y2Color}
/>
)
}
}
}
if (markerSize3 > 0) {
const r = markerSize3 * 0.5
for (let i = 0; i < numY3Points; i += xStep) {
const x = this.props.x[i]
const y = this.props.y3[i * y3ValuesPerX]
if (Number.isFinite(x) && Number.isFinite(y)) {
markers.push(
<circle
key={"marker3_" + i}
cx={transformX(x).toFixed(1)}
cy={transformY3(y).toFixed(1)}
r={r}
stroke="none"
fill={y3Color}
/>
)
}
}
}
if (markerSize4 > 0) {
const r = markerSize4 * 0.5
for (let i = 0; i < numY4Points; i += xStep) {
const x = this.props.x[i]
const y = this.props.y4[i]
if (Number.isFinite(x) && Number.isFinite(y)) {
markers.push(
<circle
key={"marker4_" + i}
cx={transformX(x).toFixed(1)}
cy={transformY4(y).toFixed(1)}
r={r}
stroke="none"
fill={y4Color}
/>
)
}
}
}
const { extraPointsX, extraPointsY, extraPointsSize, extraPointsColor } = this.props
if (extraPointsX && extraPointsY && extraPointsSize > 0) {
const r = extraPointsSize * 0.5
const count = Math.min(extraPointsX.length, extraPointsY.length)
for (let i = 0; i < count; i++) {
const x = extraPointsX[i]
const y = extraPointsY[i]
if (Number.isFinite(x) && Number.isFinite(y)) {
markers.push(
<circle
key={"extraPoints_" + i}
cx={transformX(x).toFixed(1)}
cy={transformY(y).toFixed(1)}
r={r}
stroke="none"
fill={extraPointsColor}
/>
)
}
}
}
////////////////////////////////////////////////////////////////////////////////
// Draw background color based on data series:
const bg = []
if (this.props.bgValues && this.props.bgValues.length > 0 && this.props.bgColorFunc) {
const numPoints = Math.min(this.props.x.length, this.props.bgValues.length)
if (numPoints >= 2) {
let lastX = transformX(this.props.x[0])
let lastB = this.props.bgValues[0]
for (let i = 1; i < numPoints; i += xStep) {
const b = this.props.bgValues[i]
if (b !== lastB) {
const x = (transformX(this.props.x[i-1]) + transformX(this.props.x[i])) * 0.5
const width = x - lastX
bg.push(<rect key={"bg" + (i-1)} stroke="none" fill={this.props.bgColorFunc(lastB)} x={lastX.toFixed(1)} y={axisYOffsetFixed} width={width.toFixed(1)} height={rangeHeight.toFixed(1)}/>)
lastX = x
lastB = b
}
}
const i = numPoints - 1
const x = transformX(this.props.x[i])
const width = x - lastX
bg.push(<rect key={"bg" + i} stroke="none" fill={this.props.bgColorFunc(lastB)} x={lastX.toFixed(1)} y={axisYOffsetFixed} width={width.toFixed(1)} height={rangeHeight.toFixed(1)}/>)
}
}
////////////////////////////////////////////////////////////////////////////////
// X range markers left and right:
// TODO draw markers only when active,
// but initialize drag interactions when activated after component was mounted
let leftMarkerX = 0
let leftMarkerHeight = 0
if (leftMarkerVisible) {
leftMarkerX = transformX(this.props.leftMarkerX)
leftMarkerHeight = rangeHeight
}
let rightMarkerX = 0
let rightMarkerHeight = 0
if (rightMarkerVisible) {
rightMarkerX = transformX(this.props.rightMarkerX)
rightMarkerHeight = rangeHeight
}
////////////////////////////////////////////////////////////////////////////////
// Text in diagram center:
const centerText = this.props.centerText.length === 0 ? null : (
<text
x={width * 0.5}
y={height * 0.5}
textAnchor="middle"
fontFamily={this.props.centerTextFont}
fontSize={this.props.centerTextSize}
fill={this.props.centerTextColor}
stroke="none"
>
{this.props.centerText}
</text>
)
return (
<svg ref="parent" width={width} height={height} viewBox={`0 0 ${width} ${height}`}>
<defs>
<linearGradient id="leftMarkerGradient" x1="0%" x2="100%">
<stop offset="0%" style={{stopColor: this.props.leftMarkerColor, stopOpacity: 0}}/>
<stop offset="100%" style={{stopColor: this.props.leftMarkerColor, stopOpacity: 1}}/>
</linearGradient>
<linearGradient id="rightMarkerGradient" x1="0%" x2="100%">
<stop offset="0%" style={{stopColor: this.props.rightMarkerColor, stopOpacity: 1}}/>
<stop offset="100%" style={{stopColor: this.props.rightMarkerColor, stopOpacity: 0}}/>
</linearGradient>
</defs>
{backgroundColor ? <rect fill={backgroundColor} width="100%" height="100%"/> : null}
{centerText}
<g transform={`scale(1,-1) translate(0,-${height})`} fontFamily={labelFont} fontSize={labelSize}>
{bg}
{gridLines}
<polyline stroke={this.props.axisColor} strokeWidth="1" fill="none" points={axisPoints}/>
{labels}
{bars}
{y4Points.length > 0 ? <polyline stroke={y4Color} strokeOpacity={y4Opacity} strokeWidth={this.props.y4LineWidth} strokeDasharray={this.props.y4LineDashes} fill="none" points={y4Points}/> : null}
{y3Points.length > 0 ? <polyline stroke={y3Color} strokeOpacity={y3Opacity} strokeWidth={this.props.y3LineWidth} strokeDasharray={this.props.y3LineDashes} fill="none" points={y3Points}/> : null}
{y2Points.length > 0 ? <polyline stroke={y2Color} strokeOpacity={y2Opacity} strokeWidth={this.props.y2LineWidth} strokeDasharray={this.props.y2LineDashes} fill="none" points={y2Points}/> : null}
{yPoints.length > 0 ? <polyline stroke={yColor} strokeOpacity={yOpacity} strokeWidth={this.props.yLineWidth} strokeDasharray={this.props.yLineDashes} fill="none" points={yPoints}/> : null}
{markers}
{selectedMarker}
<g ref="leftMarkerGroup">
<rect
ref="leftMarkerCover"
x={axisXOffsetFixed}
y={axisYOffsetFixed}
width={(leftMarkerX - axisXOffset).toFixed(1)}
height={leftMarkerHeight.toFixed(1)}
stroke="none"
fill={this.props.leftRightMarkerCoverColor}
fillOpacity={this.props.leftRightMarkerCoverOpacity}
/>
<rect
ref="leftMarker"
x={leftMarkerX-this.props.leftRightMarkerWidth}
y={axisYOffset}
width={this.props.leftRightMarkerWidth}
height={leftMarkerHeight.toFixed(1)}
stroke="none"
fill="url(#leftMarkerGradient)"
style={{touchAction: "none"}}
/>
</g>
<g ref="rightMarkerGroup">
<rect
ref="rightMarkerCover"
x={rightMarkerX.toFixed(1)}
y={axisYOffset.toFixed(1)}
width={(rightAxisX - rightMarkerX).toFixed(1)}
height={rightMarkerHeight.toFixed(1)}
stroke="none"
fill={this.props.leftRightMarkerCoverColor}
fillOpacity={this.props.leftRightMarkerCoverOpacity}
/>
<rect
ref="rightMarker"
x={rightMarkerX.toFixed(1)}
y={axisYOffset.toFixed(1)}
width={this.props.leftRightMarkerWidth}
height={rightMarkerHeight.toFixed(1)}
stroke="none"
fill="url(#rightMarkerGradient)"
style={{touchAction: "none"}}
/>
</g>
</g>
</svg>
)
}
}