@flourish/chart-layout
Advanced tools
Comparing version 2.0.1 to 2.1.0-prerelease.1
{ | ||
"name": "@flourish/chart-layout", | ||
"version": "2.0.1", | ||
"version": "2.1.0-prerelease.1", | ||
"description": "Create axes", | ||
@@ -5,0 +5,0 @@ "main": "chart-layout.js", |
@@ -151,2 +151,9 @@ # Flourish chart layout | ||
### `chart_layout.xTicks.autoLabelSpace([value, [unit]])`<br/>`chart_layout.yTicks.autoLabelSpace([value, [unit]])`<br/>`chart_layout.y2Ticks.autoLabelSpace([value, [unit]])` | ||
All three `*Ticks` methods outlined above have a sub-method that can be used to define how the maximum space for tick labels is calculated when the Flourish-user specifies the "auto" option for the relevant `tick_label_space_mode` property. These methods recognise up to two parameters. `value` specifies the magnitude of the margin in the given `unit`. Recognised units are "px", "rem" and "fraction". In the case of "fraction", this refers to the height (for the x method) or width of the chart-layout instance. If only a `value` is specified when calling one of these methods, the `unit` is assumed to be "px". The `instance` is returned in either case. If `value` is not specified when calling one of these functions then an object is returned that includes the current choice of `unit` and the calculated values for all three options based on the currently specified `value` and `unit` combination. | ||
The space being defined is a height for the x axis and a width for the y and y2 axes. Labels that are too long for the defined space may be automatically shorted (with trailing characters replaced by a single "…" character). However, if the labels are oriented at right angles to the relevant spatial dimension, no shortening will occur. | ||
## Other methods of `chart_layout` | ||
@@ -153,0 +160,0 @@ |
@@ -0,1 +1,8 @@ | ||
# 2.1.0 (prerelease) | ||
* Add label space settings and tick-label cropping | ||
* Rotated labels on y-axes | ||
* Better tick visibility settings | ||
* Better transitions between linear and log scales | ||
* Better setting visibility | ||
# 2.0.1 | ||
@@ -2,0 +9,0 @@ * Fix global option for scales |
@@ -72,2 +72,64 @@ var remToPx; | ||
export { updateRemToPx, remToPx, getFont, xyToTranslate, angleToRotate, linesIntersect, log10, getExponent, getSignificantDigitCount }; | ||
function sign(val) { | ||
if (!val || typeof val !== "number") return 0; | ||
return val > 0 ? 1 : -1; | ||
} | ||
// The safeScale function wraps a standard d3-scale, preventing it from returning a NaN value or +-Infinity | ||
// which can lead to errors when being used for translations during transitions - eg from a linear scale to | ||
// a log scale or vice versa | ||
function safeScale(scale) { | ||
var type = scale.type; // Store the type since scale.copy won't copy that custom property across | ||
scale = scale.copy(); // Use a copy of the scale so it can't be tampered with from outside | ||
scale.type = type; // Add the type information to the new scale | ||
var SAFE_POSITION = -1e6; // The +-1e6th pixel should not be on screen | ||
var COPIED_METHODS = ["domain", "range"]; | ||
var scaleWrapper = function(unscaled_value) { | ||
var val = scale(unscaled_value); | ||
// Catch values that will break SVG transforms | ||
if (isNaN(val) || Math.abs(val) === Infinity) { | ||
// Work out whether should be off top/left (flipped === false) or bottom/right (flipped) | ||
var flipped; | ||
var range = scale.range(); | ||
if (type !== "numeric") flipped = range[0] > range[1] ? true : false; | ||
else { | ||
var domain = scale.domain(); | ||
// flipped true for ordinary y axis or x axis is set so go largest -> smallest | ||
flipped = sign(domain[1] - domain[0]) !== sign(range[1] - range[0]); | ||
} | ||
val = SAFE_POSITION * (flipped ? -1 : 1); | ||
} | ||
return val; | ||
}; | ||
// Having the range and domain easily accessible is useful, so apply these functions | ||
COPIED_METHODS.forEach(function(name) { | ||
scaleWrapper[name] = function() { | ||
var result = scale[name].apply(scale, arguments); | ||
// Return the wrapper rather than the wrapped scale object | ||
return result === scale ? scaleWrapper : result; | ||
}; | ||
}); | ||
// Add a custom copy method and the scale property to the wrapper | ||
scaleWrapper.copy = function() { return safeScale(scale); }; | ||
scaleWrapper.type = scale.type; | ||
return scaleWrapper; | ||
} | ||
export { | ||
updateRemToPx, | ||
remToPx, | ||
getFont, | ||
xyToTranslate, | ||
angleToRotate, | ||
linesIntersect, | ||
log10, | ||
getExponent, | ||
getSignificantDigitCount, | ||
safeScale | ||
}; |
@@ -62,3 +62,2 @@ import { scaleLinear, scaleLog, scalePoint } from "d3-scale"; | ||
var scale = scaleLog().domain(extent).nice(); | ||
scale.type = "numeric"; | ||
var domain = scale.domain(); | ||
@@ -70,4 +69,6 @@ | ||
if (max > 0) domain[1] = max; | ||
scale.domain(domain); | ||
scale.type = "numeric"; | ||
return scale.domain(domain); | ||
return scale; | ||
} | ||
@@ -74,0 +75,0 @@ |
@@ -24,2 +24,4 @@ var X_DEFAULTS = Object.freeze({ | ||
tick_label_weight: "normal", | ||
tick_label_space_mode: "auto", | ||
tick_label_space: 10, | ||
@@ -26,0 +28,0 @@ gridlines_visible: false, |
@@ -26,2 +26,4 @@ import { fillInDefaults } from "./common"; | ||
tick_label_weight: "normal", | ||
tick_label_space_mode: "auto", | ||
tick_label_space: 10, | ||
@@ -28,0 +30,0 @@ gridlines_visible: true, |
import { select } from "d3-selection"; | ||
import { remToPx, getFont, getSignificantDigitCount, getExponent } from "../common"; | ||
var DUMMY_TEXT = "Testing"; | ||
function degToRad(deg) { | ||
return deg * (Math.PI / 180); | ||
} | ||
function getFitTextFunction(params) { | ||
var max_space = params.max_space !== undefined ? params.max_space : 100; | ||
var text_height = params.text_height !== undefined ? params.text_height : remToPx(1); | ||
var angle = params.angle || 0; | ||
var canvas = document.createElement("canvas"); | ||
var ctx = canvas.getContext("2d"); | ||
ctx.font = params.font; | ||
var max_width = max_space; | ||
if (angle === 90) max_width = Infinity; // No point stripping letters off if angle is 90 degrees | ||
else if (angle) { | ||
var theta = degToRad(angle); | ||
max_width = (max_space - text_height * Math.sin(theta)) / Math.cos(theta); | ||
} | ||
return function(text) { | ||
var remove_counter = 0; | ||
var new_text, width; | ||
do { | ||
new_text = remove_counter ? text.substring(0, text.length - remove_counter) + "…" : text; | ||
width = ctx.measureText(new_text).width; | ||
} | ||
while ((width > max_width) && (++remove_counter < text.length)); | ||
return { text: new_text, text_width: width }; | ||
}; | ||
} | ||
function getAutoLabelSpaceFunction(instance, dimension) { | ||
var UNITS = [ "px", "rem", "fraction" ]; | ||
dimension = dimension || "width"; | ||
var value = 0.3; | ||
var unit = "fraction"; | ||
var getValues = function() { | ||
var dim = instance[dimension](); | ||
var px = unit === "px" ? value : (unit === "rem" ? remToPx(value) : (value * dim)); | ||
return { | ||
px: px, | ||
rem: unit === "rem" ? value : px / remToPx(1), | ||
fraction: unit === "fraction" ? value : px / dim, | ||
unit: unit | ||
}; | ||
}; | ||
return function(v, u) { | ||
if (v === undefined) return getValues(); | ||
value = Math.max(v, 0); | ||
unit = UNITS.indexOf(u) !== -1 ? u : "px"; | ||
return instance; | ||
}; | ||
} | ||
function initXTicks(instance, state) { | ||
@@ -14,18 +75,24 @@ var x = state.x; | ||
var angle = +x.tick_label_angle; | ||
var theta = angle * Math.PI/180; | ||
var sinTheta = Math.sin(theta); | ||
var cosTheta = Math.cos(theta); | ||
var max_box_height = 0; | ||
var format = instance.xFormat(); | ||
var text_height; | ||
var text_selection = group.append("text") | ||
group.append("text") | ||
.style("opacity", 0) | ||
.style("font", font); | ||
.style("font", font) | ||
.each(function() { | ||
var bounds = select(this).text(DUMMY_TEXT).node().getBoundingClientRect(); | ||
text_height = bounds.height; | ||
}) | ||
.remove(); | ||
var max_space = remToPx(x.tick_label_space); | ||
if (x.tick_label_space_mode === "auto") max_space = instance.xTicks.autoLabelSpace().px; | ||
var params = { text_height: text_height, max_space: max_space, font: font, angle: 90 - angle }; | ||
var fitText = getFitTextFunction(params); | ||
ticks = tick_array.map(function(value, index) { | ||
var text = format(value); | ||
text_selection.text(text); | ||
var bounds = text_selection.node().getBoundingClientRect(); | ||
var text_width = bounds.width; | ||
var text_height = bounds.height; | ||
var result = fitText(format(value)); | ||
var text = result.text; | ||
var text_width = result.text_width; | ||
var box_width, box_height, box_width_left, box_width_right, box_height_left, box_height_right; | ||
@@ -47,6 +114,9 @@ | ||
*/ | ||
var theta = degToRad(angle); | ||
var sinTheta = Math.sin(theta); | ||
var cosTheta = Math.cos(theta); | ||
box_width_left = text_width * cosTheta; // horizontal distance between tl and tr after rotation | ||
box_width_right = text_height * sinTheta; // horizontal distance between br and tr after rotation | ||
box_height_left = text_height * sinTheta; // vertical distance between tl and tr after rotation | ||
box_height_right = text_width * cosTheta; // vertical distance between br and tr after rotation | ||
box_height_left = text_width * sinTheta; // vertical distance between tl and tr after rotation | ||
box_height_right = text_height * cosTheta; // vertical distance between br and tr after rotation | ||
box_width = box_width_left + box_width_right; // total bounding box width after rotation | ||
@@ -64,3 +134,3 @@ box_height = box_height_left + box_height_right; // total bounding box height after rotation | ||
box_height_left = box_height_right; | ||
box_height_right = box_height_left; | ||
box_height_right = temp; | ||
} | ||
@@ -89,6 +159,4 @@ | ||
text_selection.remove(); | ||
ticks.type = type; | ||
ticks.max_box_height = max_box_height; | ||
ticks.max_box_height = x.tick_label_space_mode === "fixed" ? remToPx(x.tick_label_space) : max_box_height; | ||
Object.freeze(ticks); | ||
@@ -99,7 +167,5 @@ | ||
var getTicks = function() { | ||
return ticks; | ||
}; | ||
var getTicks = function() { return ticks; }; | ||
getTicks._update = setTicks; | ||
getTicks.autoLabelSpace = getAutoLabelSpaceFunction(instance, "height"); | ||
@@ -110,3 +176,7 @@ return getTicks; | ||
function initVerticalTicks(instance, y, y_data, y_format) { | ||
function initVerticalTicks(instance, state, y_name) { | ||
var y = state[y_name]; | ||
var data_name = y_name + "Data"; | ||
var format_name = y_name + "Format"; | ||
var ticks_name = y_name + "Ticks"; | ||
var ticks = Object.freeze([]); | ||
@@ -116,27 +186,33 @@ var group = instance.chart.select(".fl-y-axes"); | ||
var setTicks = function(tick_array) { | ||
var type = instance[y_data]().string_array ? "string" : "numeric"; | ||
var type = instance[data_name]().string_array ? "string" : "numeric"; | ||
var font = getFont(select(".fl-y-axes"), y.tick_label_size, y.tick_label_weight); | ||
var angle = +y.tick_label_angle; | ||
var theta = angle * Math.PI/180; | ||
var sinTheta = Math.sin(theta); | ||
var cosTheta = Math.cos(theta); | ||
var max_box_width = 0; | ||
var format = instance[y_format](); | ||
var format = instance[format_name](); | ||
var text_height; | ||
var text_selection = group.append("text") | ||
group.append("text") | ||
.style("opacity", 0) | ||
.style("font", font); | ||
.style("font", font) | ||
.each(function() { | ||
var bounds = select(this).text(DUMMY_TEXT).node().getBoundingClientRect(); | ||
text_height = bounds.height; | ||
}) | ||
.remove(); | ||
var max_space = remToPx(y.tick_label_space); | ||
if (y.tick_label_space_mode === "auto") max_space = instance[ticks_name].autoLabelSpace().px; | ||
var params = { text_height: text_height, max_space: max_space, font: font, angle: angle }; | ||
var fitText = getFitTextFunction(params); | ||
ticks = tick_array.map(function(value, index) { | ||
var text = format(value); | ||
text_selection.text(text); | ||
var bounds = text_selection.node().getBoundingClientRect(); | ||
var text_width = bounds.width; | ||
var text_height = bounds.height; | ||
var result = fitText(format(value)); | ||
var text = result.text; | ||
var text_width = result.text_width; | ||
var box_width, box_height, box_width_above, box_width_below, box_height_above, box_height_below; | ||
if (angle === 0 || angle === 90) { | ||
box_width = angle === 0 ? text_width : text_height; | ||
box_width = !angle ? text_width : text_height; | ||
// Add extra padding for text rotated by 90 degrees | ||
box_height = angle === 0 ? text_height : text_width + remToPx(0.5); | ||
box_height = !angle ? text_height : text_width + remToPx(0.5); | ||
box_width_above = box_width; | ||
@@ -152,4 +228,7 @@ box_width_below = box_width; | ||
*/ | ||
box_width_above = text_width * sinTheta; // horizontal distance between tr and br after rotation | ||
box_width_below = text_height * cosTheta; // horizontal distance between bl and br after rotation | ||
var theta = degToRad(angle); | ||
var sinTheta = Math.sin(theta); | ||
var cosTheta = Math.cos(theta); | ||
box_width_above = text_height * sinTheta; // horizontal distance between tr and br after rotation | ||
box_width_below = text_width * cosTheta; // horizontal distance between bl and br after rotation | ||
box_height_above = text_height * cosTheta; // vertical distance between tr and br after rotation | ||
@@ -183,6 +262,4 @@ box_height_below = text_width * sinTheta; // vertical distance between bl and br after rotation | ||
text_selection.remove(); | ||
ticks.type = type; | ||
ticks.max_box_width = max_box_width; | ||
ticks.max_box_width = y.tick_label_space_mode === "fixed" ? remToPx(y.tick_label_space) : max_box_width; | ||
Object.freeze(ticks); | ||
@@ -193,7 +270,5 @@ | ||
var getTicks = function() { | ||
return ticks; | ||
}; | ||
var getTicks = function() { return ticks; }; | ||
getTicks._update = setTicks; | ||
getTicks.autoLabelSpace = getAutoLabelSpaceFunction(instance); | ||
@@ -205,9 +280,9 @@ return getTicks; | ||
function initYTicks(instance, state) { | ||
return initVerticalTicks(instance, state.y, "yData", "yFormat"); | ||
return initVerticalTicks(instance, state, "y"); | ||
} | ||
function initY2Ticks(instance, state) { | ||
return initVerticalTicks(instance, state.y2, "y2Data", "y2Format"); | ||
return initVerticalTicks(instance, state, "y2"); | ||
} | ||
export { initXTicks, initYTicks, initY2Ticks }; |
@@ -8,4 +8,7 @@ function tickSorter(a, b) { | ||
if (diff) return diff; | ||
// Then, finally, favour the smallest number | ||
diff = a.index - b.index; | ||
// Then the number with the largest absolute value | ||
diff = Math.abs(b.value) - Math.abs(a.value); | ||
if (diff) return diff; | ||
// Then, finally, favour the largest number | ||
diff = b.value - a.value; | ||
return diff; | ||
@@ -12,0 +15,0 @@ } |
import { select } from "d3-selection"; | ||
import { remToPx, xyToTranslate, angleToRotate, linesIntersect } from "../../common"; | ||
import { remToPx, xyToTranslate, angleToRotate, linesIntersect, safeScale } from "../../common"; | ||
import { tickSorter, userSelectNone } from "./common"; | ||
@@ -15,4 +15,4 @@ | ||
function setValues() { | ||
xScale = instance.xScale(); | ||
yScale = instance.yScale(); | ||
xScale = safeScale(instance.xScale()); | ||
yScale = safeScale(instance.yScale()); | ||
animation_duration = oldXScale ? instance.animationDuration() : 0; | ||
@@ -122,8 +122,3 @@ var range = xScale.range(); | ||
.duration(animation_duration) | ||
.attr("transform", function(d) { | ||
var x = exitingXScale(d.value); | ||
// Prevent errors being thrown when x is +/- Infinity | ||
if (Math.abs(x) > 1e6) x = 1e6 * (x > 0 ? 1 : -1); | ||
return xyToTranslate(x, axis_y_position); | ||
}) | ||
.attr("transform", function(d) { return xyToTranslate(exitingXScale(d.value), axis_y_position); }) | ||
.style("opacity", 0) | ||
@@ -340,8 +335,3 @@ .remove(); | ||
.duration(animation_duration) | ||
.attr("transform", function(d) { | ||
var x = exitingXScale(d.value); | ||
// Prevent errors being thrown when x is +/- Infinity | ||
if (Math.abs(x) > 1e6) x = 1e6 * (x > 0 ? 1 : -1); | ||
return xyToTranslate(x, axis_y_position); | ||
}) | ||
.attr("transform", function(d) { return xyToTranslate(exitingXScale(d.value), axis_y_position); }) | ||
.style("opacity", 0) | ||
@@ -409,4 +399,4 @@ .remove(); | ||
var p1q1 = [ x_anchor, y_anchor ]; | ||
var p2 = [ x_anchor + d.box_width_right, y_anchor + d.box_height_right]; | ||
var q2 = [ x_anchor - d.box_width_left, y_anchor + d.box_height_left]; | ||
var p2 = [ x_anchor - d.box_width_left, y_anchor - d.box_height_left]; | ||
var q2 = [ x_anchor + d.box_width_right, y_anchor - d.box_height_right]; | ||
var p = [ p1q1, p2 ]; | ||
@@ -417,6 +407,6 @@ var q = [ p1q1, q2 ]; | ||
if (x_anchor <= placed_tick.x_anchor) { | ||
if (linesIntersect(p, placed_tick.q)) return 0; | ||
if (linesIntersect(q, placed_tick.p)) return 0; | ||
} | ||
else { | ||
if (linesIntersect(placed_tick.p, q)) return 0; // eslint-disable-line no-lonely-if | ||
if (linesIntersect(p, placed_tick.q)) return 0; // eslint-disable-line no-lonely-if | ||
} | ||
@@ -423,0 +413,0 @@ } |
import { select } from "d3-selection"; | ||
import { remToPx, xyToTranslate, angleToRotate, linesIntersect } from "../../common"; | ||
import { remToPx, xyToTranslate, angleToRotate, linesIntersect, safeScale } from "../../common"; | ||
import { tickSorter, userSelectNone } from "./common"; | ||
@@ -16,4 +16,4 @@ | ||
return function() { | ||
var xScale = instance.xScale(); | ||
var yScale = instance.yScale(); | ||
var xScale = safeScale(instance.xScale()); | ||
var yScale = safeScale(instance.yScale()); | ||
var ticks = instance.yTicks(); | ||
@@ -95,8 +95,3 @@ var animation_duration = oldXScale ? instance.animationDuration() : 0; | ||
.duration(animation_duration) | ||
.attr("transform", function(d) { | ||
var y = exitingYScale(d.value); | ||
// Prevent errors being thrown when y is +/- Infinity | ||
if (Math.abs(y) > 1e6) y = 1e6 * (y > 0 ? 1 : -1); | ||
return xyToTranslate(x0, y); | ||
}) | ||
.attr("transform", function(d) { return xyToTranslate(x0, exitingYScale(d.value)); }) | ||
.style("opacity", 0) | ||
@@ -103,0 +98,0 @@ .remove(); |
import { select } from "d3-selection"; | ||
import { remToPx, xyToTranslate, angleToRotate, linesIntersect } from "../../common"; | ||
import { remToPx, xyToTranslate, angleToRotate, linesIntersect, safeScale } from "../../common"; | ||
import { tickSorter, userSelectNone } from "./common"; | ||
@@ -16,4 +16,4 @@ | ||
return function() { | ||
var xScale = instance.xScale(); | ||
var yScale = instance.y2Scale(); | ||
var xScale = safeScale(instance.xScale()); | ||
var yScale = safeScale(instance.y2Scale()); | ||
var ticks = instance.y2Ticks(); | ||
@@ -96,8 +96,3 @@ var animation_duration = oldXScale ? instance.animationDuration() : 0; | ||
.duration(animation_duration) | ||
.attr("transform", function(d) { | ||
var y = exitingYScale(d.value); | ||
// Prevent errors being thrown when y is +/- Infinity | ||
if (Math.abs(y) > 1e6) y = 1e6 * (y > 0 ? 1 : -1); | ||
return xyToTranslate(x1, y); | ||
}) | ||
.attr("transform", function(d) { return xyToTranslate(x1, exitingYScale(d.value)); }) | ||
.style("opacity", 0) | ||
@@ -104,0 +99,0 @@ .remove(); |
@@ -1,2 +0,2 @@ | ||
import { remToPx } from "../../common"; | ||
import { remToPx, safeScale } from "../../common"; | ||
@@ -26,4 +26,4 @@ var DASH_ARRAYS = { | ||
return function() { | ||
var xScale = instance.xScale(); | ||
var yScale = instance.yScale(); | ||
var xScale = safeScale(instance.xScale()); | ||
var yScale = safeScale(instance.yScale()); | ||
oldXScale = oldXScale || xScale; | ||
@@ -30,0 +30,0 @@ oldYScale = oldYScale || yScale; |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
325938
8479
198
2