diff --git a/src/visualizers/Histogram.ts b/src/visualizers/Histogram.ts index 681f9eb4..e9455a34 100644 --- a/src/visualizers/Histogram.ts +++ b/src/visualizers/Histogram.ts @@ -8,11 +8,19 @@ import {VisualizerDefault} from './VisualizerDefault' width="320" style="float: right; margin-left: 1em;" />]( ../assets/img/FactorHistogram/ExampleImage.png) -This visualizer counts the number of prime factors of each entry in the -sequence and creates a histogram of the results. The horizontal axis -represents _X_, the number of prime factors. The height of each bar shows -how many entries in the sequence have a corresponding value of _X_. -Designed by Devlin Costello. +This visualizer counts the number of prime factors (with multiplicity) +of each entry in the sequence and creates a histogram of the results. + +The number of prime factors with multiplicity is a function commonly +called +[Omega](https://oeis.org/wiki/ +Omega(n),_number_of_prime_factors_of_n_(with_multiplicity)). + +The horizontal axis represents values of Omega. Each +bar corresponds to a range of possible Omega values (a bin). +The height of each bar shows how many entries in the sequence +have a corresponding value of Omega. + ## Parameters **/ @@ -23,10 +31,13 @@ class FactorHistogramVisualizer extends VisualizerDefault { binSize = 1 terms = 100 firstIndex = NaN + mouseOver = true + + binFactorArray: number[] = [] params = { /** md -- Bin Size: The size (number of _X_ values included) for each bin +- Bin Size: The size (number of Omega values) for each bin of the histogram. **/ binSize: { @@ -36,8 +47,8 @@ class FactorHistogramVisualizer extends VisualizerDefault { required: true, }, /** md -- First Index: The first index to start getting the factors and forming the - histogram from. If the first index is before the first term +- First Index: The first index included in the statistics. + If the first index is before the first term of the series then the first term of the series will be used. **/ firstIndex: { @@ -48,7 +59,7 @@ class FactorHistogramVisualizer extends VisualizerDefault { }, /** md -- Number of Terms: The number of terms in the series after the first term. +- Number of Terms: The number of terms included in the statistics. If this goes past the last term of the sequence it will show all terms of the sequence after the first index. **/ @@ -58,6 +69,18 @@ class FactorHistogramVisualizer extends VisualizerDefault { displayName: 'Number of Terms', required: true, }, + + /** md +- Mouse Over: This turns on a mouse over feature that shows you the height + of the bin that you are currently hovering over, as well as + the bin label (i.e., which Omega values are included). + **/ + mouseOver: { + value: this.mouseOver, + forceType: 'boolean', + displayName: 'Mouse Over', + required: true, + }, } checkParameters() { @@ -93,8 +116,8 @@ class FactorHistogramVisualizer extends VisualizerDefault { return Math.trunc(input / this.binSize) } - // Create an array with the number of factor of - // each element at the corresponding index of the array + // Create an array with the number of factors of + // the element at the corresponding index of the array factorArray(): number[] { const factorArray = [] for (let i = this.startIndex(); i < this.endIndex(); i++) { @@ -117,82 +140,184 @@ class FactorHistogramVisualizer extends VisualizerDefault { // Create an array with the frequency of each number // of factors in the corresponding bins - binFactorArray(): number[] { - const binFactorArray = [] + binFactorArraySetup() { const factorArray = this.factorArray() const largestValue = factorArray.reduce( - (a, b) => Math.max(a, b), + (a: number, b: number) => Math.max(a, b), -Infinity ) for (let i = 0; i < this.binOf(largestValue) + 1; i++) { - binFactorArray.push(0) + this.binFactorArray.push(0) } for (let i = 0; i < factorArray.length; i++) { - binFactorArray[this.binOf(factorArray[i])]++ + this.binFactorArray[this.binOf(factorArray[i])]++ } - - return binFactorArray } // Create a number that represents how // many pixels wide each bin should be binWidth(): number { // 0.95 Creates a small offset from the side of the screen - if (this.binFactorArray().length <= 30) { - return (0.95 * this.sketch.width) / this.binFactorArray().length + if (this.binFactorArray.length <= 30) { + return (0.95 * this.sketch.width) / this.binFactorArray.length - 1 } else { - return (0.95 * this.sketch.width) / 30 + return (0.95 * this.sketch.width) / 30 - 1 } } // Create a number that represents how many pixels high // each increase of one in the bin array should be height(): number { - const binFactorArray = this.binFactorArray() - const greatestValue = binFactorArray.reduce( - (a, b) => Math.max(a, b), + const greatestValue = this.binFactorArray.reduce( + (a: number, b: number) => Math.max(a, b), -Infinity ) - // 0.95 Creates a small offset from the side of the screen - return (0.95 * this.sketch.width) / greatestValue + // magic number creates a small offset from the top of the screen + return (0.9 * this.sketch.height) / greatestValue + } + + // check if mouse is in the given bin + mouseOverInBin(xAxisHeight: number, binIndex: number): boolean { + if ( + this.sketch.mouseY + // hard to mouseover tiny bars; min height to catch mouse + > Math.min( + xAxisHeight + - this.height() * this.binFactorArray[binIndex], + xAxisHeight - 10 + ) + // and above axis + && this.sketch.mouseY < xAxisHeight + ) { + return true + } + return false + } + + drawHoverBox(binIndex: number, offset: number) { + const mouseX = this.sketch.mouseX + const mouseY = this.sketch.mouseY + const boxWidth = this.sketch.width * 0.15 + const textVerticalSpacing = this.sketch.textAscent() + const boxHeight = textVerticalSpacing * 2.3 + // don't want box to wander past right edge of canvas + const boxX = Math.min(mouseX, this.sketch.width - boxWidth) + const boxY = mouseY - boxHeight + const boxOffset = offset + const boxRadius = Math.floor(boxOffset) + + // create the box itself + this.sketch.fill('white') + this.sketch.rect( + boxX, + boxY, + boxWidth, + boxHeight, + boxRadius, + boxRadius, + boxRadius, + boxRadius + ) + + // Draws the text for the number of prime factors + // that bin represents + this.sketch.fill('black') + this.sketch.text( + 'Factors:', + boxX + boxOffset, + boxY + textVerticalSpacing + ) + let binText = '' + if (this.binSize != 1) { + binText = ( + this.binSize * binIndex + + '-' + + (this.binSize * (binIndex + 1) - 1) + ).toString() + } else { + binText = binIndex.toString() + } + const binTextSize = this.sketch.textWidth(binText) + 3 * boxOffset + this.sketch.text( + binText, + boxX + boxWidth - binTextSize, + boxY + textVerticalSpacing + ) + + // Draws the text for the number of elements of the sequence + // in the bin + this.sketch.text( + 'Height:', + boxX + boxOffset, + boxY + textVerticalSpacing * 2 + ) + const heightText = this.binFactorArray[binIndex].toString() + this.sketch.text( + heightText, + boxX + + boxWidth + - 3 * boxOffset + - this.sketch.textWidth(heightText), + boxY + textVerticalSpacing * 2 + ) } draw() { - // These numbers provide the rgb values for the background color - // This is light blue - this.sketch.background(176, 227, 255) + if (this.binFactorArray.length == 0) { + this.binFactorArraySetup() + } + this.sketch.background(176, 227, 255) // light blue this.sketch.textSize(0.02 * this.sketch.height) const height = this.height() const binWidth = this.binWidth() - const binFactorArray = this.binFactorArray() - const offsetScalar = 0.975 - const textOffsetScalar = 0.995 + const largeOffsetScalar = 0.945 // padding between axes and edge + const smallOffsetScalar = 0.996 + const largeOffsetNumber = (1 - largeOffsetScalar) * this.sketch.width + const smallOffsetNumber = (1 - smallOffsetScalar) * this.sketch.width + const binIndex = Math.floor( + (this.sketch.mouseX - largeOffsetNumber) / binWidth + ) + const xAxisHeight = largeOffsetScalar * this.sketch.height + + // Checks to see whether the mouse is in the bin drawn on the screen + let inBin = false + if (this.mouseOver) { + inBin = this.mouseOverInBin(xAxisHeight, binIndex) + } + + // Draw the axes + const yAxisPosition = largeOffsetNumber this.sketch.line( // Draws the y-axis - (1 - offsetScalar) * this.sketch.width, + yAxisPosition, 0, - (1 - offsetScalar) * this.sketch.width, + yAxisPosition, this.sketch.height ) this.sketch.line( // Draws the x-axis 0, - offsetScalar * this.sketch.height, + xAxisHeight, this.sketch.width, - offsetScalar * this.sketch.height + xAxisHeight ) for (let i = 0; i < 30; i++) { + if (this.mouseOver && inBin && i == binIndex) { + this.sketch.fill(200, 200, 200) + } else { + this.sketch.fill('white') + } this.sketch.rect( // Draws the rectangles for the Histogram - (1 - offsetScalar) * this.sketch.width + binWidth * i, - offsetScalar * this.sketch.height - - height * binFactorArray[i], - binWidth, - height * binFactorArray[i] + largeOffsetNumber + binWidth * i + 1, + largeOffsetScalar * this.sketch.height + - height * this.binFactorArray[i], + binWidth - 2, + height * this.binFactorArray[i] ) - if (binFactorArray.length > 30) { + if (this.binFactorArray.length > 30) { this.sketch.text( 'Too many unique factors.', this.sketch.width * 0.75, @@ -204,25 +329,82 @@ class FactorHistogramVisualizer extends VisualizerDefault { this.sketch.height * 0.05 ) } + + this.sketch.fill('black') // text must be filled if (this.binSize != 1) { - // Draws text for if the bin size is not 1 + // Draws text in the case the bin size is not 1 + const binText = ( + this.binSize * i + + ' - ' + + (this.binSize * (i + 1) - 1) + ).toString() this.sketch.text( - this.binSize * i + ' - ' + (this.binSize * (i + 1) - 1), - 1 - offsetScalar + binWidth * (i + 1 / 2), - textOffsetScalar * this.sketch.width + binText, + 1 - largeOffsetScalar + binWidth * (i + 1 / 2), + smallOffsetScalar * this.sketch.width ) } else { - // Draws text for if the bin size is 1 + // Draws text in the case the bin size is 1 + const binText = i.toString() this.sketch.text( - i, - (1 - offsetScalar) * this.sketch.width - + (binWidth * (i + 1) - binWidth / 2), - textOffsetScalar * this.sketch.width + binText, + largeOffsetNumber + (binWidth * (i + 1) - binWidth / 2), + smallOffsetScalar * this.sketch.width ) } } - this.sketch.noLoop() + let tickHeight = Math.floor( + (0.95 * this.sketch.height) / (height * 5) + ) + + // Sets the tickHeight to 1 if the calculated value is less than 1 + if (tickHeight === 0) { + tickHeight = 1 + } + // Draws the markings on the Y-axis + for (let i = 0; i < 9; i++) { + // Draws the tick marks + this.sketch.line( + (largeOffsetNumber * 3) / 4, + this.sketch.height + - largeOffsetNumber + - tickHeight * height * (i + 1), + (3 * largeOffsetNumber) / 2, + this.sketch.height + - largeOffsetNumber + - tickHeight * height * (i + 1) + ) + + // Places the numbers on the right side of the axis if + // they are 4 digits or more; left side otherwise + let tickNudge = 0 + if (tickHeight > 999) { + tickNudge = (3 * largeOffsetNumber) / 2 + } + // Avoid placing text that will get cut off + const tickYPosition = + this.sketch.height + - largeOffsetNumber + - tickHeight * height * (i + 1) + + (3 * smallOffsetNumber) / 2 + if (tickYPosition > this.sketch.textAscent()) { + this.sketch.text( + tickHeight * (i + 1), + tickNudge, + tickYPosition + ) + } + } + + // If mouse interaction, draw hover box + if (this.mouseOver === true && inBin === true) { + this.drawHoverBox(binIndex, smallOffsetNumber) + } + // If no mouse interaction, don't loop + if (this.mouseOver === false) { + this.sketch.noLoop() + } } }