Skip to main content

Graph

Do you need to plot your values in some automatic way? Here's a customizable snippet for creating your own 2D graphs.

Function

The code below generates a 2D graph as layout, with the following inputs:

  • values as a non-sorted list of SKYMATH.Vector2D to plot.
  • and these optional arguments:
    • graphType to switch between linear or scatter appearance.
    • xLabel & yLabel for the titles of each axis.
    • nrDivisions as the number of divisions for each axis in a SKYMATH.Vector2D.
    • decimals as the number of decimals in the divisions for each axis in a SKYMATH.Vector2D.
    • validStepSizes as a list of number for step sizes that the axis divisions can have.
    • valuesColor as the color of the values to plot.
    • textSize for the axis divisions.
    • labelTextSize for the labels.
    • graphSize as the visual size of the graph.
export function generateGraphLayout(
values: SKYMATH.Vector2D[],
{
graphType = 'scatter' as 'linear' | 'scatter',
xLabel = 'Time [s]',
yLabel = 'Displacement [mm]',
nrDivisions = new SKYMATH.Vector2D(11, 7),
decimals = new SKYMATH.Vector2D(1, 1),
validStepSizes = [0.1, 0.2, 0.5, 1, 1.5, 2, 3, 4, 5, 6, 7, 8, 9, 10],
valuesColor = 0xff0000, // can also be "new SKYCAD.RgbColor(255, 0, 0)"
textSize = 2,
labelTextSize = 3,
graphSize = new SKYMATH.Vector2D(100, 50),
} = {},
) {
const AXIS_MARGIN = 4
const ARROW_SIZE = new SKYMATH.Vector2D(2, 3)
const layout = new SKYCAD.Layout()

// #region Step size & Axis placement
let minX = Math.min(...values.map((p) => p.x))
let maxX = Math.max(...values.map((p) => p.x))
let minY = Math.min(...values.map((p) => p.y))
let maxY = Math.max(...values.map((p) => p.y))

if (minX === maxX) {
minX -= 1
maxX += 1
}
if (minY === maxY) {
minY -= 1
maxY += 1
}

const availableWidth = graphSize.x - 2 * AXIS_MARGIN - ARROW_SIZE.y
const availableHeight = graphSize.y - 2 * AXIS_MARGIN - ARROW_SIZE.y

let stepSizeX =
validStepSizes.find((s) => s >= (maxX - minX) / (nrDivisions.x - 1)) || (maxX - minX) / (nrDivisions.x - 1)
let stepSizeY =
validStepSizes.find((s) => s >= (maxY - minY) / (nrDivisions.y - 1)) || (maxY - minY) / (nrDivisions.y - 1)

const axisMinX = Math.floor(minX / stepSizeX) * stepSizeX
const axisMaxX = axisMinX + stepSizeX * (nrDivisions.x - 1)
const axisMinY = Math.floor(minY / stepSizeY) * stepSizeY
const axisMaxY = axisMinY + stepSizeY * (nrDivisions.y - 1)

const visualScaleX = availableWidth / (axisMaxX - axisMinX)
const visualScaleY = availableHeight / (axisMaxY - axisMinY)

const hasZeroX = axisMinX <= 0 && axisMaxX >= 0
const hasZeroY = axisMinY <= 0 && axisMaxY >= 0

const originX = hasZeroX
? -axisMinX * visualScaleX + AXIS_MARGIN
: axisMaxX <= 0
? AXIS_MARGIN + (nrDivisions.x - 1) * stepSizeX * visualScaleX
: AXIS_MARGIN

const originY = hasZeroY
? -axisMinY * visualScaleY + AXIS_MARGIN
: axisMaxY <= 0
? AXIS_MARGIN + (nrDivisions.y - 1) * stepSizeY * visualScaleY
: AXIS_MARGIN
// #endregion

// #region Values
const sortedValues = values.slice().sort((a, b) => a.x - b.x)
switch (graphType) {
case 'scatter':
sortedValues.forEach(({ x, y }) => {
const px = (x - axisMinX) * visualScaleX + AXIS_MARGIN
const py = (y - axisMinY) * visualScaleY + AXIS_MARGIN
layout.addSketch(SKYCAD.generateCircleSketch(px, py, 1), {
fillColor: valuesColor,
lineColor: valuesColor,
})
})
break
case 'linear': {
const functionSketch = new SKYCAD.Sketch()
sortedValues.forEach(({ x, y }, index) => {
const px = (x - axisMinX) * visualScaleX + AXIS_MARGIN
const py = (y - axisMinY) * visualScaleY + AXIS_MARGIN
if (index === 0) functionSketch.moveTo(px, py)
else functionSketch.lineTo(px, py)
})
layout.addSketch(functionSketch, { lineColor: valuesColor })
break
}
}
// #endregion

// #region X-axis
const xAxisSketch = new SKYCAD.Sketch()
xAxisSketch.moveTo(AXIS_MARGIN, originY)
xAxisSketch.lineTo(graphSize.x - ARROW_SIZE.y, originY)
xAxisSketch.lineTo(graphSize.x - ARROW_SIZE.y, originY + 0.5 * ARROW_SIZE.x)
xAxisSketch.lineTo(graphSize.x, originY)
xAxisSketch.lineTo(graphSize.x - ARROW_SIZE.y, originY - 0.5 * ARROW_SIZE.x)
xAxisSketch.lineTo(graphSize.x - ARROW_SIZE.y, originY)

for (let i = 0; i < nrDivisions.x; i++) {
const xValue = axisMinX + i * stepSizeX
layout.addLayout(
getDivisionLayout(xValue, {
orientation: 'vertical',
decimals: decimals.x,
textSize,
}),
{
position: new SKYMATH.Vector2D((xValue - axisMinX) * visualScaleX + AXIS_MARGIN, originY),
rotation: Math.PI / 2,
},
)
}
layout.addSketch(xAxisSketch, { fillColor: 0x000000 })
// #endregion

// #region Y-axis
const yAxisSketch = new SKYCAD.Sketch()
yAxisSketch.moveTo(originX, AXIS_MARGIN)
yAxisSketch.lineTo(originX, graphSize.y - ARROW_SIZE.y)
yAxisSketch.lineTo(originX + 0.5 * ARROW_SIZE.x, graphSize.y - ARROW_SIZE.y)
yAxisSketch.lineTo(originX, graphSize.y)
yAxisSketch.lineTo(originX - 0.5 * ARROW_SIZE.x, graphSize.y - ARROW_SIZE.y)
yAxisSketch.lineTo(originX, graphSize.y - ARROW_SIZE.y)

for (let i = 0; i < nrDivisions.y; i++) {
const yValue = axisMinY + i * stepSizeY
layout.addLayout(
getDivisionLayout(yValue, {
orientation: 'horizontal',
decimals: decimals.y,
textSize,
}),
{
position: new SKYMATH.Vector2D(originX, (yValue - axisMinY) * visualScaleY + AXIS_MARGIN),
},
)
}
layout.addSketch(yAxisSketch, { fillColor: 0x000000 })
// #endregion

// #region Labels
const layoutBounds = layout.getBounds()
layout.addText(xLabel, {
position: new SKYMATH.Vector2D(
layoutBounds.min.x + (layoutBounds.max.x - layoutBounds.min.x) / 2,
layoutBounds.min.y - 1.33 * labelTextSize,
),
size: labelTextSize,
align: 'center',
})
layout.addText(yLabel, {
position: new SKYMATH.Vector2D(
layoutBounds.min.x - labelTextSize,
layoutBounds.min.y + (layoutBounds.max.y - layoutBounds.min.y) / 2,
),
size: labelTextSize,
align: 'center',
rotation: Math.PI / 2,
})
// #endregion

return layout

function getDivisionLayout(value: number, { size = 2, orientation = 'vertical', textSize = 2, decimals = 1 } = {}) {
const isVertical = orientation === 'vertical'
const layout = new SKYCAD.Layout()
const valueSketch = new SKYCAD.Sketch()
valueSketch.moveTo(-0.5 * size, 0)
valueSketch.lineTo(0.5 * size, 0)
layout.addSketch(valueSketch)
layout.addText(`${value.toFixed(decimals)}`, {
position: new SKYMATH.Vector2D(
-0.5 * size + (isVertical ? -textSize : -0.5 * textSize),
isVertical ? 0 : -0.33 * textSize,
),
align: isVertical ? 'center' : 'right',
size: textSize,
rotation: isVertical ? -Math.PI / 2 : 0,
})
return layout
}
}

Examples

  • Values with +x and +y:
const values = [
new SKYMATH.Vector2D(30.1, 6),
new SKYMATH.Vector2D(41.4, 11.3),
new SKYMATH.Vector2D(24.1, 33.8),
new SKYMATH.Vector2D(49.4, 1.2),
new SKYMATH.Vector2D(18.7, 14.8),
new SKYMATH.Vector2D(0.5, 23.8),
new SKYMATH.Vector2D(37.2, 47.4),
new SKYMATH.Vector2D(43.8, 37.1),
new SKYMATH.Vector2D(11.3, 17.2),
new SKYMATH.Vector2D(22.5, 31.2),
]
const graphLayout = GEOM2D.generateGraphLayout(values)
  • Values with -x and -y:
const values = [
new SKYMATH.Vector2D(-5.1, -5.2),
new SKYMATH.Vector2D(-2.4, -3.5),
new SKYMATH.Vector2D(-7.3, -1.4),
new SKYMATH.Vector2D(-5, -4.4),
new SKYMATH.Vector2D(-3.3, -1),
new SKYMATH.Vector2D(-3.6, -2.1),
new SKYMATH.Vector2D(-9.4, -3.9),
new SKYMATH.Vector2D(-1.5, -3.5),
new SKYMATH.Vector2D(-7, -0.5),
new SKYMATH.Vector2D(-0.7, -3.2),
]
const graphLayout = GEOM2D.generateGraphLayout(values)
  • Values with ±x and +y:
const values = [
new SKYMATH.Vector2D(-0.5, 7.1),
new SKYMATH.Vector2D(-8.8, 8.8),
new SKYMATH.Vector2D(6.8, 7.9),
new SKYMATH.Vector2D(-5.6, 1.9),
new SKYMATH.Vector2D(-1.8, 4.6),
new SKYMATH.Vector2D(4.7, 6.4),
new SKYMATH.Vector2D(1.8, 8.2),
new SKYMATH.Vector2D(4.3, 5.3),
new SKYMATH.Vector2D(-5.3, 3.3),
new SKYMATH.Vector2D(8, 6),
]
const graphLayout = GEOM2D.generateGraphLayout(values)
  • Values with +x and ±y:
const values = [
new SKYMATH.Vector2D(39.4, -19.8),
new SKYMATH.Vector2D(27, -36.9),
new SKYMATH.Vector2D(25.5, -0.5),
new SKYMATH.Vector2D(33.2, -19.4),
new SKYMATH.Vector2D(7.1, -2.2),
new SKYMATH.Vector2D(10, -29.8),
new SKYMATH.Vector2D(30.9, 20.5),
new SKYMATH.Vector2D(40.9, -2.3),
new SKYMATH.Vector2D(34.1, 31.3),
new SKYMATH.Vector2D(41.4, 37),
]
const graphLayout = GEOM2D.generateGraphLayout(values)
  • 1 single value:
const values = [new SKYMATH.Vector2D(57, 102)]
const graphLayout = GEOM2D.generateGraphLayout(values)