Merge pull request #539 from wilkox/frequency_spectrum_plot

Add visual to Frequency Spectrum goodie
master
Jag Talon 2015-01-08 19:31:57 -05:00
commit 4e1eb737ea
6 changed files with 875 additions and 227 deletions

View File

@ -75,6 +75,8 @@ Telephony::CountryDialingCodes = 1.04
URI::Escape::XS = 0.12
DateTime::Calendar::Chinese = 1.00
DateTime::Event::Chinese = 1.00
SVG = 2.59
Lingua::EN::Inflect = 1.895
DateTime::Event::Sunrise = 0
Geo::Coordinates::DecimalDegrees = 0.09
Math::SigFigs = 1.09

View File

@ -2,10 +2,10 @@ package DDG::Goodie::FrequencySpectrum;
# ABSTRACT: describe the nature of various wave frequencies.
use strict;
use SVG;
use DDG::Goodie;
triggers end => "hz","khz","mhz","ghz","thz","hertz","kilohertz","gigahertz","megahertz","terahertz";
use Lingua::EN::Inflect qw(WORDLIST);
use Math::SigFigs qw(:all);
zci answer_type => "frequency_spectrum";
zci is_cached => 1;
@ -17,235 +17,687 @@ name 'FrequencySpectrum';
code_url 'https://github.com/duckduckgo/zeroclickinfo-goodies/blob/master/lib/DDG/Goodie/FrequencySpectrum.pm';
category 'physical_properties';
topics 'science';
attribution web => "https://machinepublishers.com",
twitter => 'machinepub';
attribution web => "https://machinepublishers.com", twitter => 'machinepub';
sub THOUSAND { 1000 };
sub MILLION { 1000000 };
sub BILLION { 1000000000 };
sub TRILLION { 1000000000000 };
#Javascript to dynamically resize and/or hide elements
my $dynamicwidths = <<EOF;
<script type="text/javascript">
#reference: https://en.wikipedia.org/wiki/Radio_spectrum
#Radio spectrum ranges along with example uses
my $radio_ranges =
[
[ "3", "29", "ELF band used by pipeline inspection gauges."],
[ "30", "299", "SLF band used by submarine communication systems."],
[ "300", "2999", "ULF band used by mine cave communication systems."],
[ "3000", "29999", "VLF band used by government time stations and navigation systems."],
[ "30000", "299999", "LF band used by AM broadcasts, government time stations, navigation systems, and weather alert systems."],
[ "300000", "2999999", "MF band used by AM broadcasts, navigation systems, and ship-to-shore communication systems."],
[ "3000000", "29999999", "HF band used by international shortwave broadcasts, aviation systems, government time stations, weather stations, and amateur radio."],
[ "30000000", "299999999", "VHF band used by FM broadcasts, televisions, amateur radio, marine communication systems, and air traffic control."],
[ "300000000", "2999999999", "UHF band used by televisions, cordless phones, cell phones, pagers, walkie-talkies, and satellites."],
[ "3000000000", "29999999999", "SHF band used by microwave ovens, wireless LANs, cell phones, and satellites."],
[ "30000000000", "299999999999", "EHF band used by radio telescopes, security screening systems, and point-to-point high-bandwidth devices."],
[ "300000000000", "3000000000000", "THF band used by satellites and radio telescopes."],
];
// Get the marker label and tag
var markerlabel, markertag
markerlabel = document.getElementById("marker_label")
markertag = document.getElementById("marker_tag")
#reference: https://en.wikipedia.org/wiki/Color
#Color ranges. Some colors are controversial but these are fairly well accepted.
my $color_ranges =
[
[ "400000000000000", "479999999999999", "red" ],
[ "480000000000000", "504999999999999", "orange" ],
[ "505000000000000", "524999999999999", "yellow" ],
[ "525000000000000", "574999999999999", "green" ],
[ "575000000000000", "609999999999999", "cyan" ],
[ "610000000000000", "667999999999999", "blue" ],
[ "668000000000000", "714999999999999", "indigo" ],
[ "715000000000000", "800000000000000", "violet" ],
];
// Firefox (and possbily other browers) have a problem with the
// getBBox function. For now, I'll work around this by simply
// hiding the marker tag if getBBox() is not available.
try {
# reference: https://en.wikipedia.org/wiki/Musical_acoustics
#Ranges for common instruments
my $instrument_ranges =
[
[ "87", "1046", "human voice" ],
[ "82.407", "329.63", "bass vocalists" ],
[ "87.307", "349.23", "baritone vocalists" ],
[ "130.81", "440.00", "tenor vocalists" ],
[ "196.00", "698.46", "alto vocalists" ],
[ "220.00", "880.00", "mezzo-soprano vocalists" ],
[ "261.63", "880.00", "soprano vocalists" ],
[ "41.203", "523.25", "double-bass" ],
[ "130.81", "1760.00", "viola" ],
[ "196.00", "2637.00", "violin" ],
[ "82.41", "1046.5", "guitar" ],
[ "196.00", "1396.9", "mandolin" ],
[ "130.81", "1046.5", "banjo" ],
[ "27.500", "4186.0", "piano" ],
[ "38.891", "440.00", "tuba" ],
[ "82.407", "523.25", "trombone" ],
[ "164.81", "932.33", "trumpet" ],
[ "207.65", "1244.5", "saxophone" ],
[ "261.63", "2093.0", "flute" ],
[ "146.83", "1864.7", "clarinet" ],
[ "58.270", "783.99", "bassoon" ],
[ "233.08", "1760.0", "oboe" ],
];
// Resize marker to fit text
bbox = markerlabel.getBBox()
markerlabel.setAttribute("x", bbox.x)
markertag.setAttribute("x", bbox.x - (bbox.width / 2))
markertag.setAttribute("y", bbox.y + 1)
markertag.setAttribute("width", bbox.width)
markertag.setAttribute("height", bbox.height)
# Reference: https://en.wikipedia.org/wiki/Ultraviolet
my $ultraviolet_ranges =
[
[ 7.495*(10**14), 3*(10**16), "UV light is found in sunlight and is emitted by electric arcs and specialized lights such as mercury lamps and black lights." ],
];
// If the marker tag is wider than the window - 80 px, hide it
if (bbox.width > (wwidth - 80)) {
markerlabel.style.visibility = "hidden"
markertag.style.visibility = "hidden"
}
# Reference: https://en.wikipedia.org/wiki/X-ray
my $xray_ranges =
[
[ 3*(10**16), 3*(10**19), "X-rays are used for various medical and industrial uses such as radiographs and CT scans. "],
];
// If getBBox() not available, hide the tag and label
} catch(err) {
markerlabel.style.visibility = "hidden"
markertag.style.visibility = "hidden"
}
// When window is too small, remove marker label and tag
// and abbreviate major range (y-axis) labels
var wwidth, majrangelabels
wwidth = Math.max(document.documentElement.clientWidth, window.innerWidth || 0)
majrangelabels = document.getElementsByClassName("major_range_label")
if (wwidth < 500) {
# Reference:
my $gamma_ranges =
[
[ 10**19, 10**24, "Gamma rays are can be used to treat cancer and for diagnostic purposes." ],
];
// Marker tag and label
markerlabel.style.visibility = "hidden"
markertag.style.visibility = "hidden"
// Major range labels
for (var i = majrangelabels.length - 1; i >= 0; i--) {
var labeltext
labeltext = majrangelabels[i].childNodes[1].childNodes[0].textContent
if (labeltext === "Radio") {
majrangelabels[i].childNodes[1].childNodes[0].textContent = "Rad."
} else if (labeltext === "Infrared") {
majrangelabels[i].childNodes[1].childNodes[0].textContent = "Inf."
} else if (labeltext === "Visible light") {
majrangelabels[i].childNodes[1].childNodes[0].textContent = "Vis."
} else if (labeltext === "Ultraviolet") {
majrangelabels[i].childNodes[1].childNodes[0].textContent = "UV"
} else if (labeltext === "X-ray") {
majrangelabels[i].childNodes[1].childNodes[0].textContent = "X-ray"
} else if (labeltext === "Gamma") {
majrangelabels[i].childNodes[1].childNodes[0].textContent = "Gam."
}
}
}
</script>
EOF
#Regex to match a valid query
# Used for trigger and later for parsing
my $frequencySpectrumQR = qr/
^(?<quantity>[\d,]+(\.\d+)?) #Number, maybe with commas, maybe a decimal
\s? #Optional space between number and unit
( #Unit can be frequency (Hz) or wavelength (nm)
(?<factor>k|kilo|m|mega|g|giga|t|tera)? #Optional SI prefix for Hz
(?:hz|hertz) #Hz
|
(?<wavelength>nanom(et(er|re)s?)?|nm) #nm
)
$/ix;
triggers query_raw => qr/$frequencySpectrumQR/i;
#The distance light travels in a vacuum in one second,
# expressed in nanometres
my $nanometreLightSecond = 2.99792458 * (10 ** 17);
#SI prefixes
my %factors = (
k => {
multiplier => 1e3,
cased => 'k'
},
m => {
multiplier => 1e6,
cased => 'M'
},
g => {
multiplier => 1e9,
cased => 'G'
},
t => {
multiplier => 1e12,
cased => 'T'
}
);
#Load electromagnetic frequency ranges
# References:
# https://en.wikipedia.org/wiki/Radio_spectrum
# https://en.wikipedia.org/wiki/Ultraviolet
# https://en.wikipedia.org/wiki/X-ray
# https://en.wikipedia.org/wiki/Color
my @electromagnetic;
foreach (split /\n/, share("electromagnetic.txt")->slurp) {
my @range = split /\t/, $_;
push @electromagnetic, {
subspectrum => $range[0],
min => $range[1],
max => $range[2],
description => $range[3]
};
}
#Frequency ranges for EM subspectra
# Hardcoded to control graphical layout
my %emSpectrum = (
'radio' => {
min => 0,
max => 3000000000000,
track => 1
},
'infrared' => {
min => 3000000000000,
max => 400000000000000,
track => 2
},
'visible light' => {
min => 400000000000000,
max => 800000000000000,
track => 3
},
'ultraviolet' => {
min => 749500000000000,
max => 30000000000000000,
track => 4
},
'x-ray' => {
min => 30000000000000000,
max => 30000000000000000000,
track => 5
},
'gamma' => {
min => 30000000000000000000,
max => 3000000000000000000000000,
track => 6
}
);
#Load audible frequency ranges
#Reference: https://en.wikipedia.org/wiki/Musical_acoustics
my @audible;
foreach (split /\n/, share("audible.txt")->slurp) {
my @range = split /\t/, $_;
push @audible, {
min => $range[0],
max => $range[1],
produced_by => $range[2]
};
}
#Query is intitially processed here. First normalize the query format,
#normalize the units, and then calculate information about the frequency range.
handle query => sub {
return unless $_ =~ m/^[\d,.]+\s\w+$/;
return unless my $freq = normalize_freq($_);
my $freq_hz;
my $hz_abbrev;
my $freq_formatted;
#Query components
(my $quantity = $+{quantity}) =~ s/,//g;
my $factor = $+{factor} || 0;
my $wavelength = $+{wavelength} || 0;
if($freq =~ m/^(.+?)\s(?:hz|hertz)$/i) {
$freq_hz = $1;
} elsif($freq =~ m/^(.+?)\s(?:khz|kilohertz)$/i) {
$freq_hz = $1 * THOUSAND;
} elsif($freq =~ m/^(.+?)\s(?:mhz|megahertz)$/i) {
$freq_hz = $1 * MILLION;
} elsif($freq =~ m/^(.+?)\s(?:ghz|gigahertz)$/i) {
$freq_hz = $1 * BILLION;
} elsif($freq =~ m/^(.+?)\s(?:thz|terahertz)$/i) {
$freq_hz = $1 * TRILLION;
} else {
#unexpected case
return;
}
#Answer components
my $freq_hz;
my $freq_formatted;
my $answer;
my $html;
if($freq_hz >= TRILLION){
$hz_abbrev = "THz";
$freq_formatted = $freq_hz / TRILLION;
} elsif($freq_hz >= BILLION) {
$hz_abbrev = "GHz";
$freq_formatted = $freq_hz / BILLION;
} elsif($freq_hz >= MILLION) {
$hz_abbrev = "MHz";
$freq_formatted = $freq_hz / MILLION;
} elsif($freq_hz >= THOUSAND) {
$hz_abbrev = "kHz";
$freq_formatted = $freq_hz / THOUSAND;
} else {
$hz_abbrev = "Hz";
$freq_formatted = $freq_hz;
}
#If wavelength provided, convert to frequency in hz
if ($wavelength) {
$wavelength = $quantity;
$freq_hz = $nanometreLightSecond / $wavelength;
my $freq_thz = FormatSigFigs($freq_hz / $factors{'t'}{'multiplier'}, 5);
$freq_formatted = "$freq_thz THz (wavelength $quantity nm)";
$freq = $freq_formatted . " " . $hz_abbrev;
return prepare_result($freq, $freq_hz);
};
#Normalize the frequency, attempting to discern between region differences
#in number formatting. Filter out clearly invalid queries.
sub normalize_freq{
my $freq = $_;
if($freq =~ /(\d+\.){2,}|([.]{2,})|([,]{2,})/) {
return;
}
# Remove commas.
$freq =~ s/,//g;
return $freq;
};
#Take the frequency and look at which ranges it falls in.
#Build up the result string.
sub prepare_result {
my $freq = $_[0];
my $freq_hz = $_[1];
my $color = match_in_ranges(int($freq_hz), $color_ranges);
my $radio = match_in_ranges(int($freq_hz), $radio_ranges) unless $color;
my $instruments = matches_in_ranges($freq_hz, $instrument_ranges) unless $color;
my $ultraviolet = matches_in_ranges($freq_hz, $ultraviolet_ranges);
my $xray = matches_in_ranges($freq_hz, $xray_ranges);
my $gamma = matches_in_ranges($freq_hz, $gamma_ranges);
my $text_result = "";
my $more_at = '';
if($radio) {
$text_result = $freq . " is a radio frequency in the " . $radio;
$more_at = 'https://en.wikipedia.org/wiki/Radio_spectrum';
} elsif($color) {
$text_result = $freq . " is an electromagnetic frequency of " . $color . " light.";
$more_at = 'https://en.wikipedia.org/wiki/Color';
}
if($instruments) {
$more_at = 'https://en.wikipedia.org/wiki/Musical_acoustics';
$instruments =~ s/,\s([a-zA-Z\s-]+)$/, and $1/;
if($radio) {
$text_result = $text_result . "\n" . $freq . " is also an audible frequency which can be produced by " . $instruments . ".";
} else {
$text_result = $freq . " is an audible frequency which can be produced by " . $instruments . ".";
}
#If frequency provided, convert to hz
} else {
my $prefix = $factor ? lc substr($factor, 0, 1) : 0;
my $factor = $factor ? $factors{$prefix}{'multiplier'} : 1;
$freq_hz = $quantity * $factor;
my $hz_formatted = $prefix ? $factors{$prefix}{'cased'} . 'Hz' : 'Hz';
$freq_formatted = $quantity . ' ' . $hz_formatted;
}
if($ultraviolet) {
$more_at = 'https://en.wikipedia.org/wiki/Ultraviolet';
$text_result = $ultraviolet;
}
if($xray) {
$more_at = 'https://en.wikipedia.org/wiki/X-ray';
$text_result = $xray;
}
if($gamma) {
$more_at = 'https://en.wikipedia.org/wiki/Gamma_ray';
$text_result = $gamma;
#Look for a match in the electromagnetic spectrum
my $emMatch = match_electromagnetic($freq_hz);
if ($emMatch) {
#Don't show result for wavelengths outside the
# visual spectrum
return if $wavelength and not $emMatch->{subspectrum} eq 'visible light';
my $emDescription = $freq_formatted . ' is a ' . $emMatch->{subspectrum} . ' frequency' . $emMatch->{description} . '.';
$answer .= $emDescription;
$html .= $emDescription;
#Add a plot to the html
#Prepare parameters
my $rangeMin = 0;
my $rangeMax = 10000000000000000000000000;
my $bandMin = $emMatch->{min};
my $bandMax = $emMatch->{max};
my $subspectrum = $emMatch->{subspectrum};
#Set up the plot panel
my $plot = generate_plot($rangeMin, $rangeMax, scalar keys %emSpectrum);
#Add a major range for each subspectrum (e.g. radio or UV)
foreach (sort {$emSpectrum{$a}{'track'} <=> $emSpectrum{$b}{'track'} } keys %emSpectrum) {
$plot = add_major_range($plot, $emSpectrum{$_}{'min'}, $emSpectrum{$_}{'max'}, $_, $emSpectrum{$_}{'track'});
}
#Add a minor range for the band (unless the band is the subspectrum)
if (! ($emSpectrum{$subspectrum}{'min'} == $bandMin && $emSpectrum{$subspectrum}{'max'} == $bandMax)) {
# NOTE: Skipping this per @wtrsld's comp, but leaving code in
# until design is finalised
#$plot = add_minor_range($plot, $bandMin, $bandMax, $emSpectrum{$subspectrum}{'track'});
}
#Add a marker for the query frequency
# Colour the marker if frequency is in visible spectrum
my $markerRGB;
if ($emMatch->{subspectrum} eq 'visible light') {
$markerRGB = frequency_to_RGB($freq_hz);
} else {
$markerRGB = '#F7614F';
}
$plot = add_marker($plot, $freq_hz, $markerRGB, $freq_formatted);
#Generate the SVG
$html .= $plot->{svg}->xmlify;
}
if($text_result) {
(my $html_result = $text_result) =~ s/\n/<br>/g;
$html_result .= "<br><a href='$more_at'>More at Wikipedia</a>";
$text_result .= "\nMore at $more_at";
return $text_result, html => $html_result, heading => "$freq (Frequency Spectrum)";
#Look for matches in the audible spectrum
# NOTE: Audible frequency results are currently being suppressed,
# as the resulting IA is too long. This will be revisited when
# better stying is available.
#my @audibleMatches = @{match_audible($freq_hz)};
my @audibleMatches = ();
if (@audibleMatches) {
my $audibleDescription = $freq_formatted . ' is';
$audibleDescription .= ' also' if $emMatch;
$audibleDescription .= ' an audible frequency which can be produced by ';
my @producers;
push @producers, $_->{produced_by} for @audibleMatches;
$audibleDescription .= WORDLIST(@producers, {cong => 'and'});
$audibleDescription .= '.';
$answer .= $audibleDescription;
$html .= $audibleDescription;
#Add a plot to the HTML
#Basic plot parameters
my $rangeMin = 10;
my $rangeMax = 10000;
#Set up the background panel
# A 'track' is a row in the plot/categorical variable on the y axis
(my $plot, my $transform) = generate_plot($rangeMin, $rangeMax, scalar @audibleMatches);
#Add a track with a major range for each producer
my $track = 0;
foreach my $match (@audibleMatches) {
++$track;
my $freqRangeMin = $match->{min};
my $freqRangeMax = $match->{max};
my $label = $match->{produced_by};
$plot = add_major_range($plot, $transform, $freqRangeMin, $freqRangeMax, $label, $track);
}
#Add a marker for the query frequency
$plot = add_marker($plot, $freq_hz, scalar @audibleMatches, '#000', $freq_formatted);
#Generate the SVG
$html .= $plot->xmlify;
}
return $answer, html => wrap_html($html) if $answer;
return;
};
#Find which single range applies.
sub match_in_ranges {
my $freq = $_[0];
my $ranges = $_[1];
foreach my $range (@$ranges) {
if($freq >= $range->[0] && $freq <= $range->[1]){
return $range->[2];
}
#Find match in the electromagnetic spectrum
sub match_electromagnetic {
my $freq_hz = shift;
foreach (@electromagnetic) {
return $_ if ($_->{min} <= $freq_hz) && ($_->{max} >= $freq_hz);
}
return;
}
return "";
};
#Find any number of ranges which apply.
sub matches_in_ranges {
my $freq = $_[0];
my $ranges = $_[1];
#Find matches in the audible spectrum
sub match_audible {
my $freq_hz = shift;
my @matches;
foreach my $range (@$ranges) {
if($freq >= $range->[0] && $freq <= $range->[1]) {
push(@matches, $range->[2]);
}
foreach (@audible) {
push @matches, $_ if ($_->{min} <= $freq_hz) && ($_->{max} >= $freq_hz);
}
return \@matches;
}
sub generate_plot {
#Set up plot parameters
my $plot = {};
#Range (passed)
$plot->{rangeMin} = $_[0];
$plot->{rangeMax} = $_[1];
#Number of tracks (passed)
$plot->{tracks} = $_[2];
#Height of a single track
$plot->{trackHeight} = 25;
#Height of a band
$plot->{bandHeight} = 18;
#Padding between edge of band and edge of track
$plot->{bandGutter} = ($plot->{trackHeight} - $plot->{bandHeight}) / 2;
#Total height of panel (area where data is plotted/Cartesian plane)
$plot->{panelHeight} = $plot->{tracks} * $plot->{trackHeight};
#Padding
$plot->{leftGutter} = 20;
$plot->{rightGutter} = 5;
$plot->{bottomGutter} = 50;
$plot->{topGutter} = 30;
#Plot width is dynamic, always expressed as percentage
$plot->{width} = 100;
#Plot height is fixed, and depends on number of tracks (passed)
# and the track height
$plot->{height} = $plot->{panelHeight} + $plot->{bottomGutter} + $plot->{topGutter};
#Initialise SVG
$plot->{svg} = SVG->new(height => $plot->{height}, class => 'zci--plot');
#If the difference betweeen the range
# minimum and maximum is two orders of
# magnitude or greater, use a log10 scale
my $log10 = int(log10($plot->{rangeMax})) - int(log10($plot->{rangeMin})) >= 2 ? 1 : 0;
#Build a transformation function to map values to
# x coordinates
my $transform;
if ($log10) {
$plot->{transform} = sub {
my $value = shift;
my $unit = ($plot->{width} - $plot->{leftGutter} - $plot->{rightGutter}) / (log10($plot->{rangeMax}) - log10($plot->{rangeMin}));
return $plot->{leftGutter} + ((log10($value) - log10($plot->{rangeMin})) * $unit);
};
} else {
$plot->{transform} = sub {
my $value = shift;
my $unit = ($plot->{width} - $plot->{leftGutter} - $plot->{rightGutter}) / ($plot->{rangeMax} - $plot->{rangeMin});
return $plot->{leftGutter} + (($value - $plot->{rangeMin}) * $unit);
};
}
return join(", ", @matches);
};
#Add plot background
$plot->{svg}->group(
class => 'plot_background',
)->rect(
width => '100%',
height => $plot->{height},
x => 0,
y => 0
);
#Add panel background
$plot->{svg}->group(
class => 'plot_panel',
)->rect(
width => ($plot->{width} - $plot->{leftGutter} - $plot->{rightGutter}) . '%',
height => $plot->{panelHeight},
x => $plot->{leftGutter} . '%',
y => $plot->{topGutter},
);
#Calculate x-axis tick locations
my @ticks;
# If we're using a log10 scale, put a tick at
# each power of 10 between range min and max
if ($log10) {
@ticks = map { 10 ** $_ } int(log10($plot->{rangeMin})) .. int(log10($plot->{rangeMax}));
#If we're using a linear scale, put a tick at every
# integer multiple at the order of magnitude of
# range max
} else {
my $order = 10 ** int(log10($plot->{rangeMax}));
@ticks = map { $_ * $order } int($plot->{rangeMin} / $order) + 1 .. int($plot->{rangeMax} / $order);
unshift(@ticks, $plot->{rangeMin}) unless $ticks[0] == $plot->{rangeMin};
push(@ticks, $plot->{rangeMax}) unless $ticks[-1] == $plot->{rangeMax};
}
#If there are more than 10 ticks, remove every
# second tick until there are 10 or fewer
while (scalar @ticks > 10) {
@ticks = @ticks[grep !($_ % 2), 0..$#ticks];
}
#Draw ticks
my $xAxis = $plot->{svg}->group (
id => 'x_axis',
);
foreach (@ticks) {
my $x = $plot->{transform}->($_);
#Draw tick line
# NOTE: Currently skipping this per wtrsld's redesign
#my $tick = $xAxis->group();
#$tick->line(
#x1 => $x . '%',
#x2 => $x . '%',
#y1 => $plot->{panelHeight} + $plot->{topGutter},
#y2 => $plot->{panelHeight} + $plot->{topGutter} + 4,
#class => 'x_axis_tick'
#);
#Annotate tick
my $text = $xAxis->text(
dy => '1em',
x => $x . '%',
y => $plot->{panelHeight} + $plot->{topGutter} + 4,
'text-anchor' => 'middle',
class => 'x_axis_text'
);
if ($log10 && $_ > 10) {
$text->tag('tspan', -cdata => '10');
$text->tag(
'tspan',
'baseline-shift' => 'super',
dy => '-0.2em', #Superscripts need an extra nudge
dx => '-0.5em', #Bring superscript close to parent
-cdata => log10($_),
style => { 'font-size' => '0.5em' },
);
} else {
$text->tag('tspan', -cdata => $_);
}
}
#Add x-axis gridlines
# NOTE: Currently skipping this per wtrsld's redesign
#my $gridlines = $plot->{svg}->group (
#class => 'x_axis_gridline',
#);
#foreach (@ticks) {
#my $x = $plot->{transform}->($_);
#my $line = $gridlines->group();
#$line->line(
#x1 => $x . '%',
#x2 => $x . '%',
#y1 => $plot->{topGutter},
#y2 => $plot->{panelHeight} + $plot->{topGutter}
#);
#}
#Add a label to the x-axis
my $xAxisLabel = $xAxis->text(
dy => '1em',
x => '50%',
y => $plot->{panelHeight} + $plot->{topGutter} + 25,
'text-anchor' => 'middle',
class => 'x_axis_label'
);
$xAxisLabel->tag('tspan', -cdata => 'Frequency (Hz)');
#Add axis lines
my $axislines = $plot->{svg}->group (
class => 'axis_line',
);
my $xaxisline = $axislines->group();
$xaxisline->line(
x1 => $plot->{transform}->(0) . '%',
x2 => $plot->{transform}->($plot->{rangeMax}) . '%',
y1 => $plot->{panelHeight} + $plot->{topGutter},
y2 => $plot->{panelHeight} + $plot->{topGutter}
);
my $yaxisline = $axislines->group();
$yaxisline->line(
x1 => $plot->{transform}->(0) . '%',
x2 => $plot->{transform}->(0) . '%',
y1 => $plot->{topGutter},
y2 => $plot->{topGutter} + $plot->{panelHeight}
);
return($plot);
}
#Add a minor range to a plot panel
sub add_minor_range {
(my $plot, my $rangeMin, my $rangeMax, my $track) = @_;
#Add rectangle for range
my $minorRange = $plot->{svg}->group(id => 'minor_range_' . $track);
my $minorRangeRect = $minorRange->group();
$minorRangeRect->rect(
class => 'minor_range',
x => $plot->{transform}->($rangeMin) . '%',
width => $plot->{transform}->($rangeMax) - $plot->{transform}->($rangeMin) . '%',
y => ($plot->{trackHeight} * ($track - 1)) + $plot->{bandGutter} + $plot->{topGutter},
height => $plot->{bandHeight},
);
return $plot;
}
#Add a major frequency range to a plot panel
sub add_major_range {
(my $plot, my $rangeMin, my $rangeMax, my $label, my $track) = @_;
#Add rectangle for range
my $majorRange = $plot->{svg}->group(id => 'major_range_' . $label);
my $majorRangeRect = $majorRange->group();
$majorRangeRect->rect(
class => 'major_range',
x => $plot->{transform}->($rangeMin) . '%',
width => $plot->{transform}->($rangeMax) - $plot->{transform}->($rangeMin) . '%',
y => ($plot->{trackHeight} * ($track - 1)) + $plot->{bandGutter} + $plot->{topGutter},
height => $plot->{bandHeight},
);
#Add label for range on the y-axis
my $x;
my $anchor;
$x = $plot->{leftGutter} - 1;
$anchor = 'end';
my $majorRangeLabel = $majorRange->group();
my $majorRangeLabelText = $majorRangeLabel->text(
x => $x . '%',
y => ($plot->{trackHeight} * ($track - 1)) + (2 * $plot->{bandGutter}) + $plot->{topGutter},
dy => ($plot->{trackHeight} / 2) - 5,
'text-anchor' => $anchor,
class => 'major_range_label'
);
$majorRangeLabel->tag('tspan', -cdata => ucfirst($label));
return $plot;
}
#Add a marker (vertical line) to a plot panel
sub add_marker {
(my $plot, my $markerValue, my $RGB, my $freq_formatted) = @_;
#Add marker rect
my $markerWidth = 1; #This is dynamically resized by $dynamicwidths
my $markerHeight = 14;
my $markerGutter = ($plot->{topGutter} - $markerHeight - 1) / 2;
$plot->{svg}->group(
class => 'marker_tag',
)->rect(
id => 'marker_tag',
width => $markerWidth,
height => $markerHeight,
x => $plot->{transform}->($markerValue) - ($markerWidth / 2) . '%',
y => $plot->{topGutter} - $markerGutter - $markerHeight + 1, #Extra pixel to account for plot border
style => { 'fill' => $RGB }
);
#Add marker label
my $markerLabel = $plot->{svg}->group();
my $markerLabelText = $markerLabel->text(
x => $plot->{transform}->($markerValue) . '%',
y => $plot->{topGutter} - $markerGutter - ($markerHeight / 2) + 4,
'text-anchor' => 'middle',
class => 'marker_label'
);
$markerLabel->tag('tspan', id => 'marker_label', -cdata => ucfirst($freq_formatted));
#Add marker line
$plot->{svg}->group(
class => 'marker'
)->line(
id => 'marker',
x1 => $plot->{transform}->($markerValue) . '%',
x2 => $plot->{transform}->($markerValue) . '%',
y1 => $plot->{topGutter} - $markerGutter,
y2 => $plot->{topGutter} + $plot->{panelHeight},
style => { 'stroke' => $RGB },
);
return $plot;
}
#Wrap html
sub wrap_html {
return <<EOF;
<!--[if lte IE 8]><div class="ie8-display-none">
<![endif]-->
<!--[if gte IE 9]><!-->
<div class='zci--conversions text--primary'>$_[0]</div>
<![endif]-->
$dynamicwidths
EOF
}
#Get log10 of a number
sub log10 {
my $n = shift;
return 0 if $n == 0;
return log($n)/log(10);
}
#Convert a visible light frequency in Hz to RGB
# Adapted from http://www.efg2.com/Lab/ScienceAndEngineering/Spectra.htm
sub frequency_to_RGB {
my $frequency = shift;
my $wavelength = $nanometreLightSecond / $frequency;
my @RGB;
if (($wavelength >= 380) && ($wavelength < 440)) {
@RGB = (
-($wavelength - 440) / (440 - 380),
0,
1,
);
} elsif (($wavelength >= 440) && ($wavelength < 490)) {
@RGB = (
0,
($wavelength - 440) / (490 - 440),
1,
);
} elsif (($wavelength >= 490) && ($wavelength < 510)) {
@RGB = (
0,
1,
-($wavelength - 510) / (510 - 490),
);
} elsif (($wavelength >= 510) && ($wavelength < 580)) {
@RGB = (
($wavelength - 510) / (580 - 510),
1,
0,
);
} elsif (($wavelength >= 580) && ($wavelength < 645)) {
@RGB = (
1,
-($wavelength - 645) / (645 - 580),
0,
);
} elsif (($wavelength >= 645) && ($wavelength <= 780)) {
@RGB = (1, 0, 0);
} else {
@RGB = (0, 0, 0);
}
#Convert to hex
return '#' . join('', map { sprintf "%02x", $_ * 255 } @RGB);
}
1;

View File

@ -0,0 +1,22 @@
87 1046 human voice
82.407 329.63 bass vocalists
87.307 349.23 baritone vocalists
130.81 440.00 tenor vocalists
196.00 698.46 alto vocalists
220.00 880.00 mezzo-soprano vocalists
261.63 880.00 soprano vocalists
41.203 523.25 double-bass
130.81 1760.00 viola
196.00 2637.00 violin
82.41 1046.5 guitar
196.00 1396.9 mandolin
130.81 1046.5 banjo
27.500 4186.0 piano
38.891 440.00 tuba
82.407 523.25 trombone
164.81 932.33 trumpet
207.65 1244.5 saxophone
261.63 2093.0 flute
146.83 1864.7 clarinet
58.270 783.99 bassoon
233.08 1760.0 oboe

View File

@ -0,0 +1,24 @@
radio 3 30 in the ELF band used by pipeline inspection gauges
radio 30 300 in the SLF band used by submarine communication systems
radio 300 3000 in the ULF band used by mine cave communication systems
radio 3000 30000 in the VLF band used by government time stations and navigation systems
radio 30000 300000 in the LF band used by AM broadcasts, government time stations, navigation systems, and weather alert systems
radio 300000 3000000 in the MF band used by AM broadcasts, navigation systems, and ship-to-shore communication systems
radio 3000000 30000000 in the HF band used by international shortwave broadcasts, aviation systems, government time stations, weather stations, and amateur radio
radio 30000000 300000000 in the VHF band used by FM broadcasts, televisions, amateur radio, marine communication systems, and air traffic control
radio 300000000 3000000000 in the UHF band used by televisions, cordless phones, cell phones, pagers, walkie-talkies, and satellites
radio 3000000000 30000000000 in the SHF band used by microwave ovens, wireless LANs, cell phones, and satellites
radio 30000000000 300000000000 in the EHF band used by radio telescopes, security screening systems, and point-to-point high-bandwidth devices
radio 300000000000 3000000000000 in the THF band used by satellites and radio telescopes
infrared 3000000000000 400000000000000 . Infrared light is found in sunlight and has a variety of industrial, commercial, scientific and household uses
visible light 400000000000000 480000000000000 corresponding to the color red
visible light 480000000000000 505000000000000 corresponding to the color orange
visible light 505000000000000 525000000000000 corresponding to the color yellow
visible light 525000000000000 575000000000000 corresponding to the color green
visible light 575000000000000 610000000000000 corresponding to the color cyan
visible light 610000000000000 668000000000000 corresponding to the color blue
visible light 668000000000000 715000000000000 corresponding to the color indigo
visible light 715000000000000 800000000000000 corresponding to the color violet
ultraviolet 749500000000000 30000000000000000 . UV light is found in sunlight and is emitted by electric arcs and specialized lights such as mercury lamps and black lights
x-ray 30000000000000000 30000000000000000000 . X-rays are used for various medical and industrial uses such as radiographs and CT scans
gamma 30000000000000000000 3000000000000000000000000 . Gamma rays can be used to treat cancer and for diagnostic purposes

View File

@ -0,0 +1,80 @@
/* Conditional for IE8 and lower */
.ie8-display-none {
display: none;
}
.zci--answer .zci--conversions {
font-size: 1.5em;
font-weight: 300;
padding-top: .25em;
padding-bottom: .25em;
}
.zci--conversions {
width: 90%;
height: 50;
}
.zci--plot {
margin-top: 1em;
}
.zci--conversions .plot_background {
fill: #FFFFFF;
stroke: #D1D1D1;
stroke-width: 1px;
}
.zci--conversions .plot_panel {
fill: #F6F6F6;
}
.zci--conversions .axis_line {
stroke: #DCDCDC;
}
.zci--conversions .x_axis_tick {
stroke: #DCDCDC;
}
.zci--conversions .x_axis_text {
font-size: 0.7em;
fill: #303030;
}
.zci--conversions .x_axis_label {
font-size: 0.6em;
fill: #ACACAC;
}
.zci--conversions .x_axis_gridline {
stroke: #DCDCDC;
}
.zci--conversions .major_range {
fill: #9DCFE1;
stroke: #9DCFE1;
}
.zci--conversions .major_range_label {
font-size: 0.7em;
fill: #303030;
}
.zci--conversions .minor_range {
fill: #80C4DE;
}
.zci--conversions .marker {
stroke-width: 2px;
stroke-opacity: 1;
}
.zci--conversions .marker_tag {
opacity: 1;
}
.zci--conversions .marker_label {
font-size: 0.5em;
fill: #FFFFFF;
}

View File

@ -1,5 +1,9 @@
#!/usr/bin/env perl
# NOTE: Audible frequency results are currently being suppressed,
# as the resulting IA is too long. This will be revisited when
# better stying is available.
use strict;
use warnings;
@ -11,39 +15,103 @@ zci is_cached => 1;
ddg_goodie_test(
['DDG::Goodie::FrequencySpectrum'],
#Primary example
'50 hz' => test_zci(
'50 Hz is a radio frequency in the SLF band used by submarine communication systems.
50 Hz is also an audible frequency which can be produced by double-bass, piano, and tuba.
More at https://en.wikipedia.org/wiki/Musical_acoustics',
html => "50 Hz is a radio frequency in the SLF band used by submarine communication systems.<br>50 Hz is also an audible frequency which can be produced by double-bass, piano, and tuba.<br><a href='https://en.wikipedia.org/wiki/Musical_acoustics'>More at Wikipedia</a>",
heading => '50 Hz (Frequency Spectrum)'
#qr/radio.+SLF.+audible.+double-bass.+piano.+tuba/,
qr/radio/,
html => qr/radio/
),
#Secondary example
'400 thz' => test_zci(
'400 THz is an electromagnetic frequency of red light.
More at https://en.wikipedia.org/wiki/Color',
html => "400 THz is an electromagnetic frequency of red light.<br><a href='https://en.wikipedia.org/wiki/Color'>More at Wikipedia</a>",
heading => '400 THz (Frequency Spectrum)'
qr/infrared/,
html => qr/infrared/
),
'4 thz' => undef,
#Misc
'1,000 hz' => test_zci(
'1 kHz is a radio frequency in the ULF band used by mine cave communication systems.
1 kHz is also an audible frequency which can be produced by human voice, viola, violin, guitar, mandolin, banjo, piano, saxophone, flute, clarinet, and oboe.
More at https://en.wikipedia.org/wiki/Musical_acoustics',
html => "1 kHz is a radio frequency in the ULF band used by mine cave communication systems.<br>1 kHz is also an audible frequency which can be produced by human voice, viola, violin, guitar, mandolin, banjo, piano, saxophone, flute, clarinet, and oboe.<br><a href='https://en.wikipedia.org/wiki/Musical_acoustics'>More at Wikipedia</a>",
heading => '1 kHz (Frequency Spectrum)'
#qr/radio.+audible.+human.+voice.+viola.+violin.+guitar.+mandolin.+banjo.+piano.+saxophone.+flute.+clarinet.+oboe/,
qr/radio/,
html => qr/radio.+/
),
'1000000.99 hz' => test_zci(
'1.00000099 MHz is a radio frequency in the MF band used by AM broadcasts, navigation systems, and ship-to-shore communication systems.
More at https://en.wikipedia.org/wiki/Radio_spectrum',
html => "1.00000099 MHz is a radio frequency in the MF band used by AM broadcasts, navigation systems, and ship-to-shore communication systems.<br><a href='https://en.wikipedia.org/wiki/Radio_spectrum'>More at Wikipedia</a>",
heading => '1.00000099 MHz (Frequency Spectrum)',
qr/radio.+MF/,
html => qr/radio.+MF/
),
'29.1 hz' => test_zci(
qr/radio.+ELF/,
html => qr/radio.+ELF/
),
#No whitespace between number and unit
'50hz' => test_zci(
#qr/radio.+SLF.+audible.+double-bass.+piano.+tuba/,
qr/radio/,
html => qr/radio/
),
'400terahertz' => test_zci(
qr/infrared/,
html => qr/infrared/
),
#Mixed case
'400 THz' => test_zci(
qr/infrared/,
html => qr/infrared/
),
'1000 HZ' => test_zci(
#qr/radio.+audible.+human.+voice.+viola.+violin.+guitar.+mandolin.+banjo.+piano.+saxophone.+flute.+clarinet.+oboe/,
qr/radio/,
html => qr/radio.+/
),
#Commas in number
'1,000,000.99 hz' => test_zci(
qr/radio.+MF/,
html => qr/radio.+MF/
),
#Can you test with all the colours of the wind?
'650 nm' => test_zci(
qr/visible.+red/,
html => qr/visible.+red/
),
'610 nanometers' => test_zci(
qr/visible.+orange/,
html => qr/visible.+orange/
),
'580 nanometres' => test_zci(
qr/visible.+yellow/,
html => qr/visible.+yellow/
),
'536 nanometer' => test_zci(
qr/visible.+green/,
html => qr/visible.+green/
),
'478.1 nm' => test_zci(
qr/visible.+blue/,
html => qr/visible.+blue/
),
'380.000000000 nanometres' => test_zci(
qr/visible.+violet/,
html => qr/visible.+violet/
),
#Only visible light wavelengths should trigger
'0.1 nm' => undef,
'100 nm' => undef,
'800 nm' => undef,
'10000 nm' => undef,
#Malformed frequencies/wavelengths should not trigger
'1000.000..99 hz' => undef,
'15 kilo hertz' => undef,
'100,123 jiggahz' => undef,
'hertz' => undef,
'terahz' => undef,
'600 nmeters' => undef,
);
done_testing;