Skip to content
This repository was archived by the owner on Oct 13, 2021. It is now read-only.

WDFN-67 - Create a symlog-like graph, using a "compound scale". #89

Merged
merged 3 commits into from
Feb 14, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
},
"globals": {
"document": true,
"Math": true,
"XMLHttpRequest": true,
"window": true
}
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ dist/
downloads/
eggs/
.eggs/
lib/
./lib/
lib64/
parts/
sdist/
Expand Down
6 changes: 0 additions & 6 deletions assets/src/scripts/components/hydrograph/points.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,6 @@ const lineSegmentsSelector = memoize(tsDataKey => createSelector(
continue;
}

// Temporary check to help detect test sites.
if (pt.qualifiers.length > 1) {
/*eslint no-console: "allow"*/
console.error('Point has multiple qualifiers', pt.qualifiers);
}

// Classes to put on the line with this point.
const lineClasses = {
approved: pt.approved,
Expand Down
27 changes: 17 additions & 10 deletions assets/src/scripts/components/hydrograph/scales.js
Original file line number Diff line number Diff line change
@@ -1,22 +1,30 @@
const { extent } = require('d3-array');
const { scaleLinear, scaleTime } = require('d3-scale');
const { scaleTime } = require('d3-scale');
const { createSelector, defaultMemoize: memoize } = require('reselect');

const { default: scaleSymlog } = require('../../lib/symlog');
const { layoutSelector, MARGIN } = require('./layout');

const paddingRatio = 0.2;


/**
* Return domainExtent padded on both ends by paddingRatio
* @param {Array} domainExtent - array of two numbers
* Return domain padded on both ends by paddingRatio.
* For positive domains, a zero-lower bound on the y-axis is enforced.
* @param {Array} domain - array of two numbers
* @return {Array} - array of two numbers
*/
function extendDomain(domainExtent) {
const padding = paddingRatio * (domainExtent[1] - domainExtent[0]);
return [domainExtent[0] - padding, domainExtent[1] + padding];
function extendDomain(domain) {
const padding = paddingRatio * (domain[1] - domain[0]);
const isPositive = domain[0] >= 0 && domain[1] >= 0;
return [
// If all values are above zero, make a-axis zero the lower bound
isPositive ? Math.max(0, domain[0] - padding) : domain[0] - padding,
domain[1] + padding
];
}


/**
* Create an x-scale oriented on the left
* @param {Array} values - Array contains {time, ...}
Expand Down Expand Up @@ -69,10 +77,9 @@ function createYScale(tsData, showSeries, ySize) {
yExtent = [0, 1];
}

// yScale is oriented on the bottom
return scaleLinear()
.range([ySize, 0])
.domain(yExtent);
return scaleSymlog()
.domain(yExtent)
.range([ySize, 0]);
}


Expand Down
1 change: 1 addition & 0 deletions assets/src/scripts/components/hydrograph/scales.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,6 @@ describe('Charting scales', () => {
expect(yScale(1)).not.toBeNaN();
expect(yScale(.5)).not.toBeNaN();
expect(yScale(.999)).not.toBeNaN();
expect(yScale(0)).not.toBeNaN();
});
});
47 changes: 47 additions & 0 deletions assets/src/scripts/lib/compound.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
export default function compound() {
var scales = [].slice.call(arguments);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm just curious about the var vs let here?

Also, it seems that this stylistically a bit different from the rest of the javascript... 🤔

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a copy-paste of the code I submitted to d3-scale, so I wrote it in the D3 style.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah. Makes sense.

if (scales.length === 0) {
return null;
}

function scale(x) {
for (var i = 0; i < scales.length; i++) {
var domain = scales[i].domain();
if (Math.min.apply(null, domain) <= x && x <= Math.max.apply(null, domain)) {
return scales[i](x);
}
}
// Fallback to last scale
return scales[scales.length - 1](x);
}

scale.domain = function() {
if (arguments.length) {
throw 'Setting a domain is not supported on compound scales';
}
var values = [].concat.apply([], scales.map(function(s) { return s.domain(); }));
var domain = [Math.min.apply(null, values), Math.max.apply(null, values)];
if (values[0] > values[1]) domain = domain.slice().reverse();
return domain;
};

scale.range = function() {
if (arguments.length) {
throw 'Setting a range is not supported on compound scales';
}
var values = [].concat.apply([], scales.map(function(s) { return s.range(); }));
var range = [Math.min.apply(null, values), Math.max.apply(null, values)];
if (values[0] > values[1]) range = range.slice().reverse();
return range;
};

scale.copy = function() {
return compound.apply(null, scales.map(function(s) { return s.copy(); }));
};

scale.scales = function(_) {
return arguments.length ? (scales = _, scale) : scales;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not understanding this syntax. Can you describe what this does?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the function is called without arguments, it returns the scales array. Otherwise, it sets the array to the passed in parameters and then returns the scale object, enabling function chaining. That clause is using the comma operator: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Comma_Operator

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. Really need to read up on ES6 syntax.

};

return scale;
}
83 changes: 83 additions & 0 deletions assets/src/scripts/lib/symlog.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
const { default: compound } = require('./compound');
const { scaleLinear: linear, scaleLog: log } = require('d3-scale');

function intersection(r1, r2) {
var reverse = r1[0] > r1[1];
if (reverse) r1 = r1.slice().reverse();

var min = r1[0] < r2[0] ? r1 : r2;
var max = min === r1 ? r2 : r1;
if (min[1] < max[0]) return null;

var section = [max[0], min[1] < max[1] ? min[1] : max[1]];
if (section[0] === section[1]) return null;

if (reverse) section = section.slice().reverse();
return section;
}

function rescale(range, domain) {
var logScale = log(),
d,
parts = [];

// Negative log scale
if (d = intersection(domain, [Number.NEGATIVE_INFINITY, -1])) {
parts.push({
domain: d,
type: log,
extent: logScale(Math.abs(d[1] - d[0]) + 1) - logScale(1)
});
}

// Linear scale
if (d = intersection(domain, [-1, 1])) {
parts.push({
domain: d,
type: linear,
extent: Math.abs(d[1] - d[0]) * (logScale(2) - logScale(1))
});
}

// Positive log scale
if (d = intersection(domain, [1, Number.POSITIVE_INFINITY])) {
parts.push({
domain: d,
type: log,
extent: logScale(Math.abs(d[1] - d[0]) + 1) - logScale(1)
});
}

// Create the scales
var scales = [];
var rangeSize = range[1] - range[0];
var rangeExtent = parts.reduce(function (acc, part) { return part.extent + acc; }, 0);
var rangeStart = range[0];
for (var i = 0; i < parts.length; i++) {
var part = parts[i];
if (part.extent > 0) {
var ratio = part.extent / rangeExtent;
var next = (i === parts.length - 1) ? range[1] : rangeStart + ratio * rangeSize;
scales.push(part.type().domain(part.domain).range([rangeStart, next]));
rangeStart = next;
}
}

return scales;
}

export default function symlog() {
var scale = compound(linear()),
compoundRange = scale.range,
compoundDomain = scale.domain;

scale.domain = function(_) {
return arguments.length ? scale.scales(rescale(scale.range(), _)) : compoundDomain();
};

scale.range = function(_) {
return arguments.length ? scale.scales(rescale(_, scale.domain())) : compoundRange();
};

return scale;
}
1 change: 1 addition & 0 deletions waterdata/templates/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ <h5>Example site pages</h5>
<li><a href="{{ url_for('monitoring_location', site_no='08279500') }}">08279500 - Rio Grande at Embudo, NM</a></li>
<li><a href="{{ url_for('monitoring_location', site_no='08470400', agency_cd='USIBW') }}">08470400 - USIBW Arroyo Colorado at Harlingen, TX</a></li>
<li><a href="{{ url_for('monitoring_location', site_no='08470400', agency_cd='USGS') }}">08470400 - Arroyo Colorado at Harlingen, TX</a></li>
<li><a href="{{ url_for('monitoring_location', site_no='251549080251200') }}">251549080251200 - Manatee Bay Creek Near Homestead, Fl - Tidal Stream</a></li>
<li><a href="{{ url_for('monitoring_location', site_no='08470400') }}">08470400 - Multiple sites</a></li>
</ul>
</div>
Expand Down