diff --git a/azure-pipelines.yml b/azure-pipelines.yml index e65c8734bb..df7d49147b 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -191,9 +191,11 @@ jobs: env: # Set `BOOST_ROOT` manually, as `BOOST_ROOT` is removed in the VM: # https://github.com/actions/virtual-environments/issues/687 - BOOST_ROOT: "C:/hostedtoolcache/windows/Boost/1.69.0/" - BOOST_INCLUDEDIR: "C:/hostedtoolcache/windows/Boost/1.69.0/include" - BOOST_LIBRARYDIR: "C:/hostedtoolcache/windows/Boost/1.69.0/libs" + # 06/18/2020 - seems like boost got moved to `x86_64` inside + # the boost folder, which broke builds for a bit. + BOOST_ROOT: "C:/hostedtoolcache/windows/Boost/1.69.0/x86_64/" + BOOST_INCLUDEDIR: "C:/hostedtoolcache/windows/Boost/1.69.0/x86_64/include" + BOOST_LIBRARYDIR: "C:/hostedtoolcache/windows/Boost/1.69.0/x86_64/libs" - job: 'Mac' diff --git a/cpp/perspective/CMakeLists.txt b/cpp/perspective/CMakeLists.txt index 7bb9d7dd19..5f4c66c26b 100644 --- a/cpp/perspective/CMakeLists.txt +++ b/cpp/perspective/CMakeLists.txt @@ -302,7 +302,7 @@ elseif(PSP_CPP_BUILD OR PSP_PYTHON_BUILD) # must be set to `NEW` to allow BOOST_ROOT to be defined by env var cmake_policy(SET CMP0074 NEW) - if(DEFINED(ENV{BOOST_ROOT})) + if(DEFINED (ENV{BOOST_ROOT})) set(Boost_NO_BOOST_CMAKE TRUE) message(WARNING "${Cyan}BOOST_ROOT: $ENV{BOOST_ROOT} ${ColorReset}") diff --git a/packages/perspective-jupyterlab/src/less/index.less b/packages/perspective-jupyterlab/src/less/index.less index bf4949c584..54d90640a3 100644 --- a/packages/perspective-jupyterlab/src/less/index.less +++ b/packages/perspective-jupyterlab/src/less/index.less @@ -12,8 +12,7 @@ div.PSPContainer-dark { flex: 1; } - -div.PSPContainer perspective-viewer{ +div.PSPContainer perspective-viewer { .perspective-viewer-material(); } diff --git a/packages/perspective-viewer-d3fc/src/config/d3fc.watch.config.js b/packages/perspective-viewer-d3fc/src/config/d3fc.watch.config.js index 0758cd9186..ea29319ef4 100644 --- a/packages/perspective-viewer-d3fc/src/config/d3fc.watch.config.js +++ b/packages/perspective-viewer-d3fc/src/config/d3fc.watch.config.js @@ -1,15 +1,15 @@ -const pluginConfig = require("./d3fc.plugin.config"); - -const rules = []; //pluginConfig.module.rules.slice(0); -rules.push({ - test: /\.js$/, - exclude: /node_modules/, - loader: "babel-loader" -}); - -module.exports = Object.assign({}, pluginConfig, { - entry: "./src/js/index.js", - module: Object.assign({}, pluginConfig.module, { - rules - }) -}); +const pluginConfig = require("./d3fc.plugin.config"); + +const rules = []; //pluginConfig.module.rules.slice(0); +rules.push({ + test: /\.js$/, + exclude: /node_modules/, + loader: "babel-loader" +}); + +module.exports = Object.assign({}, pluginConfig, { + entry: "./src/js/index.js", + module: Object.assign({}, pluginConfig.module, { + rules + }) +}); diff --git a/packages/perspective-viewer-d3fc/src/js/axis/axisFactory.js b/packages/perspective-viewer-d3fc/src/js/axis/axisFactory.js index 04a5d7c294..a70708e94e 100644 --- a/packages/perspective-viewer-d3fc/src/js/axis/axisFactory.js +++ b/packages/perspective-viewer-d3fc/src/js/axis/axisFactory.js @@ -1,140 +1,140 @@ -/****************************************************************************** - * - * Copyright (c) 2017, the Perspective Authors. - * - * This file is part of the Perspective library, distributed under the terms of - * the Apache License 2.0. The full license can be found in the LICENSE file. - * - */ -import * as fc from "d3fc"; -import {axisType} from "./axisType"; -import * as none from "./noAxis"; -import * as linear from "./linearAxis"; -import * as time from "./timeAxis"; -import * as ordinal from "./ordinalAxis"; - -const axisTypes = { - none, - ordinal, - time, - linear -}; - -export const axisFactory = settings => { - let excludeType = null; - let orient = "horizontal"; - let settingName = "crossValues"; - let settingValue = null; - let valueNames = ["crossValue"]; - - const optionalParams = ["include", "paddingStrategy", "pad"]; - const optional = {}; - - const _factory = data => { - const useType = axisType(settings) - .excludeType(excludeType) - .settingName(settingName) - .settingValue(settingValue)(); - - const axis = axisTypes[useType]; - const domainFunction = axis.domain().valueNames(valueNames); - - optionalParams.forEach(p => { - if (optional[p] && domainFunction[p]) domainFunction[p](optional[p]); - }); - if (domainFunction.orient) domainFunction.orient(orient); - - const domain = domainFunction(data); - const component = axis.component ? createComponent(axis, domain, data) : defaultComponent(); - - return { - scale: axis.scale(), - domain, - domainFunction, - labelFunction: axis.labelFunction, - component: { - bottom: component.bottom, - left: component.left, - top: component.top, - right: component.right - }, - size: component.size, - decorate: component.decorate, - label: settings[settingName].map(v => v.name).join(", "), - tickFormatFunction: axis.tickFormatFunction - }; - }; - - const createComponent = (axis, domain, data) => - axis - .component(settings) - .orient(orient) - .settingName(settingName) - .domain(domain)(data); - - const defaultComponent = () => ({ - bottom: fc.axisBottom, - left: fc.axisLeft, - top: fc.axisTop, - right: fc.axisRight, - decorate: () => {} - }); - - _factory.excludeType = (...args) => { - if (!args.length) { - return excludeType; - } - excludeType = args[0]; - return _factory; - }; - - _factory.orient = (...args) => { - if (!args.length) { - return orient; - } - orient = args[0]; - return _factory; - }; - - _factory.settingName = (...args) => { - if (!args.length) { - return settingName; - } - settingName = args[0]; - return _factory; - }; - _factory.settingValue = (...args) => { - if (!args.length) { - return settingValue; - } - settingValue = args[0]; - return _factory; - }; - - _factory.valueName = (...args) => { - if (!args.length) { - return valueNames[0]; - } - valueNames = [args[0]]; - return _factory; - }; - _factory.valueNames = (...args) => { - if (!args.length) { - return valueNames; - } - valueNames = args[0]; - return _factory; - }; - - optionalParams.forEach(p => { - _factory[p] = (...args) => { - if (!args.length) { - return optional[p]; - } - optional[p] = args[0]; - return _factory; - }; - }); - - return _factory; -}; +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ +import * as fc from "d3fc"; +import {axisType} from "./axisType"; +import * as none from "./noAxis"; +import * as linear from "./linearAxis"; +import * as time from "./timeAxis"; +import * as ordinal from "./ordinalAxis"; + +const axisTypes = { + none, + ordinal, + time, + linear +}; + +export const axisFactory = settings => { + let excludeType = null; + let orient = "horizontal"; + let settingName = "crossValues"; + let settingValue = null; + let valueNames = ["crossValue"]; + + const optionalParams = ["include", "paddingStrategy", "pad"]; + const optional = {}; + + const _factory = data => { + const useType = axisType(settings) + .excludeType(excludeType) + .settingName(settingName) + .settingValue(settingValue)(); + + const axis = axisTypes[useType]; + const domainFunction = axis.domain().valueNames(valueNames); + + optionalParams.forEach(p => { + if (optional[p] && domainFunction[p]) domainFunction[p](optional[p]); + }); + if (domainFunction.orient) domainFunction.orient(orient); + + const domain = domainFunction(data); + const component = axis.component ? createComponent(axis, domain, data) : defaultComponent(); + + return { + scale: axis.scale(), + domain, + domainFunction, + labelFunction: axis.labelFunction, + component: { + bottom: component.bottom, + left: component.left, + top: component.top, + right: component.right + }, + size: component.size, + decorate: component.decorate, + label: settings[settingName].map(v => v.name).join(", "), + tickFormatFunction: axis.tickFormatFunction + }; + }; + + const createComponent = (axis, domain, data) => + axis + .component(settings) + .orient(orient) + .settingName(settingName) + .domain(domain)(data); + + const defaultComponent = () => ({ + bottom: fc.axisBottom, + left: fc.axisLeft, + top: fc.axisTop, + right: fc.axisRight, + decorate: () => {} + }); + + _factory.excludeType = (...args) => { + if (!args.length) { + return excludeType; + } + excludeType = args[0]; + return _factory; + }; + + _factory.orient = (...args) => { + if (!args.length) { + return orient; + } + orient = args[0]; + return _factory; + }; + + _factory.settingName = (...args) => { + if (!args.length) { + return settingName; + } + settingName = args[0]; + return _factory; + }; + _factory.settingValue = (...args) => { + if (!args.length) { + return settingValue; + } + settingValue = args[0]; + return _factory; + }; + + _factory.valueName = (...args) => { + if (!args.length) { + return valueNames[0]; + } + valueNames = [args[0]]; + return _factory; + }; + _factory.valueNames = (...args) => { + if (!args.length) { + return valueNames; + } + valueNames = args[0]; + return _factory; + }; + + optionalParams.forEach(p => { + _factory[p] = (...args) => { + if (!args.length) { + return optional[p]; + } + optional[p] = args[0]; + return _factory; + }; + }); + + return _factory; +}; diff --git a/packages/perspective-viewer-d3fc/src/js/axis/axisLabel.js b/packages/perspective-viewer-d3fc/src/js/axis/axisLabel.js index 97c7b857d6..79babdf92b 100644 --- a/packages/perspective-viewer-d3fc/src/js/axis/axisLabel.js +++ b/packages/perspective-viewer-d3fc/src/js/axis/axisLabel.js @@ -1,42 +1,42 @@ -/****************************************************************************** - * - * Copyright (c) 2017, the Perspective Authors. - * - * This file is part of the Perspective library, distributed under the terms of - * the Apache License 2.0. The full license can be found in the LICENSE file. - * - */ -import {rebindAll} from "d3fc"; -import {axisType} from "./axisType"; -import {labelFunction as noLabel} from "./noAxis"; -import {labelFunction as timeLabel} from "./timeAxis"; -import {labelFunction as linearLabel} from "./linearAxis"; -import {labelFunction as ordinalLabel} from "./ordinalAxis"; - -const labelFunctions = { - none: noLabel, - ordinal: ordinalLabel, - time: timeLabel, - linear: linearLabel -}; - -export const labelFunction = settings => { - const base = axisType(settings); - let valueName = "__ROW_PATH__"; - - const label = (d, i) => { - return labelFunctions[base()](valueName)(d, i); - }; - - rebindAll(label, base); - - label.valueName = (...args) => { - if (!args.length) { - return valueName; - } - valueName = args[0]; - return label; - }; - - return label; -}; +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ +import {rebindAll} from "d3fc"; +import {axisType} from "./axisType"; +import {labelFunction as noLabel} from "./noAxis"; +import {labelFunction as timeLabel} from "./timeAxis"; +import {labelFunction as linearLabel} from "./linearAxis"; +import {labelFunction as ordinalLabel} from "./ordinalAxis"; + +const labelFunctions = { + none: noLabel, + ordinal: ordinalLabel, + time: timeLabel, + linear: linearLabel +}; + +export const labelFunction = settings => { + const base = axisType(settings); + let valueName = "__ROW_PATH__"; + + const label = (d, i) => { + return labelFunctions[base()](valueName)(d, i); + }; + + rebindAll(label, base); + + label.valueName = (...args) => { + if (!args.length) { + return valueName; + } + valueName = args[0]; + return label; + }; + + return label; +}; diff --git a/packages/perspective-viewer-d3fc/src/js/axis/axisSplitter.js b/packages/perspective-viewer-d3fc/src/js/axis/axisSplitter.js index 0692f246a3..f7c2ce7d22 100644 --- a/packages/perspective-viewer-d3fc/src/js/axis/axisSplitter.js +++ b/packages/perspective-viewer-d3fc/src/js/axis/axisSplitter.js @@ -1,94 +1,94 @@ -/****************************************************************************** - * - * Copyright (c) 2017, the Perspective Authors. - * - * This file is part of the Perspective library, distributed under the terms of - * the Apache License 2.0. The full license can be found in the LICENSE file. - * - */ -import {splitterLabels} from "./splitterLabels"; - -export const axisSplitter = (settings, sourceData, splitFn = dataSplitFunction) => { - let color; - let data; - let altData; - - // splitMainValues is an array of main-value names to put into the alt-axis - const splitMainValues = settings.splitMainValues || []; - const altValue = name => { - const split = name.split("|"); - return splitMainValues.includes(split[split.length - 1]); - }; - - const haveSplit = settings["mainValues"].some(m => altValue(m.name)); - - // Split the data into main and alt displays - data = haveSplit ? splitFn(sourceData, key => !altValue(key)) : sourceData; - altData = haveSplit ? splitFn(sourceData, altValue) : null; - - // Renderer to show the special controls for moving between axes - const splitter = selection => { - if (settings["mainValues"].length === 1) return; - - const labelsInfo = settings["mainValues"].map((v, i) => ({ - index: i, - name: v.name - })); - const mainLabels = labelsInfo.filter(v => !altValue(v.name)); - const altLabels = labelsInfo.filter(v => altValue(v.name)); - - const labeller = () => splitterLabels(settings).color(color); - - selection.select(".y-label-container>.y-label").call(labeller().labels(mainLabels)); - selection.select(".y2-label-container>.y-label").call( - labeller() - .labels(altLabels) - .alt(true) - ); - }; - - splitter.color = (...args) => { - if (!args.length) { - return color; - } - color = args[0]; - return splitter; - }; - - splitter.haveSplit = () => haveSplit; - - splitter.data = (...args) => { - if (!args.length) { - return data; - } - data = args[0]; - return splitter; - }; - splitter.altData = (...args) => { - if (!args.length) { - return altData; - } - altData = args[0]; - return splitter; - }; - - return splitter; -}; - -export const dataSplitFunction = (sourceData, isIncludedFn) => { - return sourceData.map(d => d.filter(v => isIncludedFn(v.key))); -}; - -export const dataBlankFunction = (sourceData, isIncludedFn) => { - return sourceData.map(series => { - if (!isIncludedFn(series.key)) { - // Blank this data - return series.map(v => Object.assign({}, v, {mainValue: null})); - } - return series; - }); -}; - -export const groupedBlankFunction = (sourceData, isIncludedFn) => { - return sourceData.map(group => dataBlankFunction(group, isIncludedFn)); -}; +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ +import {splitterLabels} from "./splitterLabels"; + +export const axisSplitter = (settings, sourceData, splitFn = dataSplitFunction) => { + let color; + let data; + let altData; + + // splitMainValues is an array of main-value names to put into the alt-axis + const splitMainValues = settings.splitMainValues || []; + const altValue = name => { + const split = name.split("|"); + return splitMainValues.includes(split[split.length - 1]); + }; + + const haveSplit = settings["mainValues"].some(m => altValue(m.name)); + + // Split the data into main and alt displays + data = haveSplit ? splitFn(sourceData, key => !altValue(key)) : sourceData; + altData = haveSplit ? splitFn(sourceData, altValue) : null; + + // Renderer to show the special controls for moving between axes + const splitter = selection => { + if (settings["mainValues"].length === 1) return; + + const labelsInfo = settings["mainValues"].map((v, i) => ({ + index: i, + name: v.name + })); + const mainLabels = labelsInfo.filter(v => !altValue(v.name)); + const altLabels = labelsInfo.filter(v => altValue(v.name)); + + const labeller = () => splitterLabels(settings).color(color); + + selection.select(".y-label-container>.y-label").call(labeller().labels(mainLabels)); + selection.select(".y2-label-container>.y-label").call( + labeller() + .labels(altLabels) + .alt(true) + ); + }; + + splitter.color = (...args) => { + if (!args.length) { + return color; + } + color = args[0]; + return splitter; + }; + + splitter.haveSplit = () => haveSplit; + + splitter.data = (...args) => { + if (!args.length) { + return data; + } + data = args[0]; + return splitter; + }; + splitter.altData = (...args) => { + if (!args.length) { + return altData; + } + altData = args[0]; + return splitter; + }; + + return splitter; +}; + +export const dataSplitFunction = (sourceData, isIncludedFn) => { + return sourceData.map(d => d.filter(v => isIncludedFn(v.key))); +}; + +export const dataBlankFunction = (sourceData, isIncludedFn) => { + return sourceData.map(series => { + if (!isIncludedFn(series.key)) { + // Blank this data + return series.map(v => Object.assign({}, v, {mainValue: null})); + } + return series; + }); +}; + +export const groupedBlankFunction = (sourceData, isIncludedFn) => { + return sourceData.map(group => dataBlankFunction(group, isIncludedFn)); +}; diff --git a/packages/perspective-viewer-d3fc/src/js/axis/axisType.js b/packages/perspective-viewer-d3fc/src/js/axis/axisType.js index d7c757f8c6..da34d09595 100644 --- a/packages/perspective-viewer-d3fc/src/js/axis/axisType.js +++ b/packages/perspective-viewer-d3fc/src/js/axis/axisType.js @@ -1,73 +1,73 @@ -/****************************************************************************** - * - * Copyright (c) 2017, the Perspective Authors. - * - * This file is part of the Perspective library, distributed under the terms of - * the Apache License 2.0. The full license can be found in the LICENSE file. - * - */ - -export const AXIS_TYPES = { - none: "none", - ordinal: "ordinal", - time: "time", - linear: "linear" -}; - -export const axisType = settings => { - let settingName = "crossValues"; - let settingValue = null; - let excludeType = null; - - const getType = () => { - const checkTypes = types => { - const list = settingValue ? settings[settingName].filter(s => settingValue == s.name) : settings[settingName]; - - if (settingName == "crossValues" && list.length > 1) { - // can't do multiple values on non-ordinal cross-axis - return false; - } - - return list.some(s => types.includes(s.type)); - }; - - if (settings[settingName].length === 0) { - return AXIS_TYPES.none; - } else if (excludeType != AXIS_TYPES.time && checkTypes(["datetime", "date"])) { - return AXIS_TYPES.time; - } else if (excludeType != AXIS_TYPES.linear && checkTypes(["integer", "float"])) { - return AXIS_TYPES.linear; - } - - if (excludeType == AXIS_TYPES.ordinal) { - return AXIS_TYPES.linear; - } - return AXIS_TYPES.ordinal; - }; - - getType.settingName = (...args) => { - if (!args.length) { - return settingName; - } - settingName = args[0]; - return getType; - }; - - getType.settingValue = (...args) => { - if (!args.length) { - return settingValue; - } - settingValue = args[0]; - return getType; - }; - - getType.excludeType = (...args) => { - if (!args.length) { - return excludeType; - } - excludeType = args[0]; - return getType; - }; - - return getType; -}; +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ + +export const AXIS_TYPES = { + none: "none", + ordinal: "ordinal", + time: "time", + linear: "linear" +}; + +export const axisType = settings => { + let settingName = "crossValues"; + let settingValue = null; + let excludeType = null; + + const getType = () => { + const checkTypes = types => { + const list = settingValue ? settings[settingName].filter(s => settingValue == s.name) : settings[settingName]; + + if (settingName == "crossValues" && list.length > 1) { + // can't do multiple values on non-ordinal cross-axis + return false; + } + + return list.some(s => types.includes(s.type)); + }; + + if (settings[settingName].length === 0) { + return AXIS_TYPES.none; + } else if (excludeType != AXIS_TYPES.time && checkTypes(["datetime", "date"])) { + return AXIS_TYPES.time; + } else if (excludeType != AXIS_TYPES.linear && checkTypes(["integer", "float"])) { + return AXIS_TYPES.linear; + } + + if (excludeType == AXIS_TYPES.ordinal) { + return AXIS_TYPES.linear; + } + return AXIS_TYPES.ordinal; + }; + + getType.settingName = (...args) => { + if (!args.length) { + return settingName; + } + settingName = args[0]; + return getType; + }; + + getType.settingValue = (...args) => { + if (!args.length) { + return settingValue; + } + settingValue = args[0]; + return getType; + }; + + getType.excludeType = (...args) => { + if (!args.length) { + return excludeType; + } + excludeType = args[0]; + return getType; + }; + + return getType; +}; diff --git a/packages/perspective-viewer-d3fc/src/js/axis/chartFactory.js b/packages/perspective-viewer-d3fc/src/js/axis/chartFactory.js index abe147e0d8..25c72ab091 100644 --- a/packages/perspective-viewer-d3fc/src/js/axis/chartFactory.js +++ b/packages/perspective-viewer-d3fc/src/js/axis/chartFactory.js @@ -1,163 +1,163 @@ -/****************************************************************************** - * - * Copyright (c) 2017, the Perspective Authors. - * - * This file is part of the Perspective library, distributed under the terms of - * the Apache License 2.0. The full license can be found in the LICENSE file. - * - */ -import * as d3 from "d3"; -import * as fc from "d3fc"; - -export const chartSvgFactory = (xAxis, yAxis) => chartFactory(xAxis, yAxis, fc.chartSvgCartesian, false); -export const chartCanvasFactory = (xAxis, yAxis) => chartFactory(xAxis, yAxis, fc.chartCanvasCartesian, true); - -const chartFactory = (xAxis, yAxis, cartesian, canvas) => { - let axisSplitter = null; - let altAxis = null; - - const chart = cartesian({ - xScale: xAxis.scale, - yScale: yAxis.scale, - xAxis: xAxis.component, - yAxis: yAxis.component - }) - .xDomain(xAxis.domain) - .xLabel(xAxis.label) - .xAxisHeight(xAxis.size) - .xDecorate(xAxis.decorate) - .xTickFormat(xAxis.tickFormatFunction) - .yDomain(yAxis.domain) - .yLabel(yAxis.label) - .yAxisWidth(yAxis.size) - .yDecorate(yAxis.decorate) - .yOrient("left") - .yTickFormat(yAxis.tickFormatFunction); - - if (xAxis.decorate) chart.xDecorate(xAxis.decorate); - if (yAxis.decorate) chart.yDecorate(yAxis.decorate); - - // Padding defaults can be overridden - chart.xPaddingInner && chart.xPaddingInner(1); - chart.xPaddingOuter && chart.xPaddingOuter(0.5); - chart.yPaddingInner && chart.yPaddingInner(1); - chart.yPaddingOuter && chart.yPaddingOuter(0.5); - - chart.axisSplitter = (...args) => { - if (!args.length) { - return axisSplitter; - } - axisSplitter = args[0]; - return chart; - }; - - chart.altAxis = (...args) => { - if (!args.length) { - return altAxis; - } - altAxis = args[0]; - return chart; - }; - - const oldDecorate = chart.decorate(); - chart.decorate((container, data) => { - const plotArea = container.select("d3fc-svg.plot-area"); - - plotArea - .select("svg") - .node() - .setAttribute("viewBox", `0 0 ${plotArea.node().clientWidth} ${plotArea.node().clientHeight}`); - - oldDecorate(container, data); - if (!axisSplitter) return; - - if (axisSplitter.haveSplit()) { - // Render a second axis on the right of the chart - const altData = axisSplitter.altData(); - - const y2AxisDataJoin = fc.dataJoin("d3fc-svg", "y2-axis").key(d => d); - const ySeriesDataJoin = fc.dataJoin("g", "y-series").key(d => d); - - // Column 5 of the grid - container - .enter() - .append("div") - .attr("class", "y2-label-container") - .style("grid-column", 5) - .style("-ms-grid-column", 5) - .style("grid-row", 3) - .style("-ms-grid-row", 3) - .style("width", altAxis.size || "1em") - .style("display", "flex") - .style("align-items", "center") - .style("justify-content", "center") - .append("div") - .attr("class", "y-label") - .style("transform", "rotate(-90deg)"); - - const y2Scale = altAxis.scale.domain(altAxis.domain); - const yAxisComponent = altAxis.component.right(y2Scale); - yAxisComponent.tickFormat(altAxis.tickFormatFunction); - if (altAxis.decorate) yAxisComponent.decorate(altAxis.decorate); - - // Render the axis - y2AxisDataJoin(container, ["right"]) - .attr("class", d => `y-axis ${d}-axis`) - .on("measure", (d, i, nodes) => { - const {width, height} = d3.event.detail; - if (d === "left") { - d3.select(nodes[i]) - .select("svg") - .attr("viewBox", `${-width} 0 ${width} ${height}`); - } - y2Scale.range([height, 0]); - }) - .on("draw", (d, i, nodes) => { - d3.select(nodes[i]) - .select("svg") - .call(yAxisComponent); - }); - - // Render all the series using either the primary or alternate - // y-scales - if (canvas) { - const drawMultiCanvasSeries = selection => { - const canvasPlotArea = chart.plotArea(); - canvasPlotArea.context(selection.node().getContext("2d")).xScale(xAxis.scale); - - const yScales = [yAxis.scale, y2Scale]; - [data, altData].forEach((d, i) => { - canvasPlotArea.yScale(yScales[i]); - canvasPlotArea(d); - }); - }; - - container.select("d3fc-canvas.plot-area").on("draw", (d, i, nodes) => { - drawMultiCanvasSeries(d3.select(nodes[i]).select("canvas")); - }); - } else { - const drawMultiSvgSeries = selection => { - const svgPlotArea = chart.plotArea(); - svgPlotArea.xScale(xAxis.scale); - - const yScales = [yAxis.scale, y2Scale]; - ySeriesDataJoin(selection, [data, altData]).each((d, i, nodes) => { - svgPlotArea.yScale(yScales[i]); - d3.select(nodes[i]) - .datum(d) - .call(svgPlotArea); - }); - }; - - container.select("d3fc-svg.plot-area").on("draw", (d, i, nodes) => { - drawMultiSvgSeries(d3.select(nodes[i]).select("svg")); - }); - } - } - - // Render any UI elements the splitter component requires - axisSplitter(container); - }); - - return chart; -}; +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ +import * as d3 from "d3"; +import * as fc from "d3fc"; + +export const chartSvgFactory = (xAxis, yAxis) => chartFactory(xAxis, yAxis, fc.chartSvgCartesian, false); +export const chartCanvasFactory = (xAxis, yAxis) => chartFactory(xAxis, yAxis, fc.chartCanvasCartesian, true); + +const chartFactory = (xAxis, yAxis, cartesian, canvas) => { + let axisSplitter = null; + let altAxis = null; + + const chart = cartesian({ + xScale: xAxis.scale, + yScale: yAxis.scale, + xAxis: xAxis.component, + yAxis: yAxis.component + }) + .xDomain(xAxis.domain) + .xLabel(xAxis.label) + .xAxisHeight(xAxis.size) + .xDecorate(xAxis.decorate) + .xTickFormat(xAxis.tickFormatFunction) + .yDomain(yAxis.domain) + .yLabel(yAxis.label) + .yAxisWidth(yAxis.size) + .yDecorate(yAxis.decorate) + .yOrient("left") + .yTickFormat(yAxis.tickFormatFunction); + + if (xAxis.decorate) chart.xDecorate(xAxis.decorate); + if (yAxis.decorate) chart.yDecorate(yAxis.decorate); + + // Padding defaults can be overridden + chart.xPaddingInner && chart.xPaddingInner(1); + chart.xPaddingOuter && chart.xPaddingOuter(0.5); + chart.yPaddingInner && chart.yPaddingInner(1); + chart.yPaddingOuter && chart.yPaddingOuter(0.5); + + chart.axisSplitter = (...args) => { + if (!args.length) { + return axisSplitter; + } + axisSplitter = args[0]; + return chart; + }; + + chart.altAxis = (...args) => { + if (!args.length) { + return altAxis; + } + altAxis = args[0]; + return chart; + }; + + const oldDecorate = chart.decorate(); + chart.decorate((container, data) => { + const plotArea = container.select("d3fc-svg.plot-area"); + + plotArea + .select("svg") + .node() + .setAttribute("viewBox", `0 0 ${plotArea.node().clientWidth} ${plotArea.node().clientHeight}`); + + oldDecorate(container, data); + if (!axisSplitter) return; + + if (axisSplitter.haveSplit()) { + // Render a second axis on the right of the chart + const altData = axisSplitter.altData(); + + const y2AxisDataJoin = fc.dataJoin("d3fc-svg", "y2-axis").key(d => d); + const ySeriesDataJoin = fc.dataJoin("g", "y-series").key(d => d); + + // Column 5 of the grid + container + .enter() + .append("div") + .attr("class", "y2-label-container") + .style("grid-column", 5) + .style("-ms-grid-column", 5) + .style("grid-row", 3) + .style("-ms-grid-row", 3) + .style("width", altAxis.size || "1em") + .style("display", "flex") + .style("align-items", "center") + .style("justify-content", "center") + .append("div") + .attr("class", "y-label") + .style("transform", "rotate(-90deg)"); + + const y2Scale = altAxis.scale.domain(altAxis.domain); + const yAxisComponent = altAxis.component.right(y2Scale); + yAxisComponent.tickFormat(altAxis.tickFormatFunction); + if (altAxis.decorate) yAxisComponent.decorate(altAxis.decorate); + + // Render the axis + y2AxisDataJoin(container, ["right"]) + .attr("class", d => `y-axis ${d}-axis`) + .on("measure", (d, i, nodes) => { + const {width, height} = d3.event.detail; + if (d === "left") { + d3.select(nodes[i]) + .select("svg") + .attr("viewBox", `${-width} 0 ${width} ${height}`); + } + y2Scale.range([height, 0]); + }) + .on("draw", (d, i, nodes) => { + d3.select(nodes[i]) + .select("svg") + .call(yAxisComponent); + }); + + // Render all the series using either the primary or alternate + // y-scales + if (canvas) { + const drawMultiCanvasSeries = selection => { + const canvasPlotArea = chart.plotArea(); + canvasPlotArea.context(selection.node().getContext("2d")).xScale(xAxis.scale); + + const yScales = [yAxis.scale, y2Scale]; + [data, altData].forEach((d, i) => { + canvasPlotArea.yScale(yScales[i]); + canvasPlotArea(d); + }); + }; + + container.select("d3fc-canvas.plot-area").on("draw", (d, i, nodes) => { + drawMultiCanvasSeries(d3.select(nodes[i]).select("canvas")); + }); + } else { + const drawMultiSvgSeries = selection => { + const svgPlotArea = chart.plotArea(); + svgPlotArea.xScale(xAxis.scale); + + const yScales = [yAxis.scale, y2Scale]; + ySeriesDataJoin(selection, [data, altData]).each((d, i, nodes) => { + svgPlotArea.yScale(yScales[i]); + d3.select(nodes[i]) + .datum(d) + .call(svgPlotArea); + }); + }; + + container.select("d3fc-svg.plot-area").on("draw", (d, i, nodes) => { + drawMultiSvgSeries(d3.select(nodes[i]).select("svg")); + }); + } + } + + // Render any UI elements the splitter component requires + axisSplitter(container); + }); + + return chart; +}; diff --git a/packages/perspective-viewer-d3fc/src/js/axis/domainMatchOrigins.js b/packages/perspective-viewer-d3fc/src/js/axis/domainMatchOrigins.js index 79db504dca..dd0b0387b3 100644 --- a/packages/perspective-viewer-d3fc/src/js/axis/domainMatchOrigins.js +++ b/packages/perspective-viewer-d3fc/src/js/axis/domainMatchOrigins.js @@ -1,16 +1,16 @@ -export default (domain1, domain2) => { - if (!isMatchable(domain1) || !isMatchable(domain2)) return; - - const ratio1 = originRatio(domain1); - const ratio2 = originRatio(domain2); - - if (ratio1 > ratio2) { - domain2[0] = adjustLowerBound(domain2, ratio1); - } else { - domain1[0] = adjustLowerBound(domain1, ratio2); - } -}; - -const isMatchable = domain => domain.length === 2 && !isNaN(domain[0]) && !isNaN(domain[1]) && domain[0] !== domain[1]; -const originRatio = domain => (0 - domain[0]) / (domain[1] - domain[0]); -const adjustLowerBound = (domain, ratio) => (ratio * domain[1]) / (ratio - 1); +export default (domain1, domain2) => { + if (!isMatchable(domain1) || !isMatchable(domain2)) return; + + const ratio1 = originRatio(domain1); + const ratio2 = originRatio(domain2); + + if (ratio1 > ratio2) { + domain2[0] = adjustLowerBound(domain2, ratio1); + } else { + domain1[0] = adjustLowerBound(domain1, ratio2); + } +}; + +const isMatchable = domain => domain.length === 2 && !isNaN(domain[0]) && !isNaN(domain[1]) && domain[0] !== domain[1]; +const originRatio = domain => (0 - domain[0]) / (domain[1] - domain[0]); +const adjustLowerBound = (domain, ratio) => (ratio * domain[1]) / (ratio - 1); diff --git a/packages/perspective-viewer-d3fc/src/js/axis/flatten.js b/packages/perspective-viewer-d3fc/src/js/axis/flatten.js index 88b5084d68..e8dba048cd 100644 --- a/packages/perspective-viewer-d3fc/src/js/axis/flatten.js +++ b/packages/perspective-viewer-d3fc/src/js/axis/flatten.js @@ -1,25 +1,25 @@ -/****************************************************************************** - * - * Copyright (c) 2017, the Perspective Authors. - * - * This file is part of the Perspective library, distributed under the terms of - * the Apache License 2.0. The full license can be found in the LICENSE file. - * - */ - -export const flattenExtent = array => { - const withUndefined = fn => (a, b) => { - if (a === undefined) return b; - if (b === undefined) return a; - return fn(a, b); - }; - return array.reduce((r, v) => [withUndefined(Math.min)(r[0], v[0]), withUndefined(Math.max)(r[1], v[1])], [undefined, undefined]); -}; - -export const flattenArray = array => { - if (Array.isArray(array)) { - return [].concat(...array.map(flattenArray)); - } else { - return [array]; - } -}; +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ + +export const flattenExtent = array => { + const withUndefined = fn => (a, b) => { + if (a === undefined) return b; + if (b === undefined) return a; + return fn(a, b); + }; + return array.reduce((r, v) => [withUndefined(Math.min)(r[0], v[0]), withUndefined(Math.max)(r[1], v[1])], [undefined, undefined]); +}; + +export const flattenArray = array => { + if (Array.isArray(array)) { + return [].concat(...array.map(flattenArray)); + } else { + return [array]; + } +}; diff --git a/packages/perspective-viewer-d3fc/src/js/axis/linearAxis.js b/packages/perspective-viewer-d3fc/src/js/axis/linearAxis.js index e62e5906f4..9cf6acbae6 100644 --- a/packages/perspective-viewer-d3fc/src/js/axis/linearAxis.js +++ b/packages/perspective-viewer-d3fc/src/js/axis/linearAxis.js @@ -1,73 +1,73 @@ -/****************************************************************************** - * - * Copyright (c) 2017, the Perspective Authors. - * - * This file is part of the Perspective library, distributed under the terms of - * the Apache License 2.0. The full license can be found in the LICENSE file. - * - */ -import * as d3 from "d3"; -import * as fc from "d3fc"; -import {flattenArray} from "./flatten"; -import {extentLinear as customExtent} from "../d3fc/extent/extentLinear"; -import valueformatter from "./valueFormatter"; - -export const scale = () => d3.scaleLinear(); - -export const domain = () => { - const base = customExtent() - .pad([0, 0.1]) - .padUnit("percent"); - - let valueNames = ["crossValue"]; - - const _domain = data => { - base.accessors(valueNames.map(v => d => parseFloat(d[v]))); - - return getDataExtent(flattenArray(data)); - }; - - fc.rebindAll(_domain, base); - - const getMinimumGap = data => { - const gaps = valueNames.map(valueName => - data - .map(d => d[valueName]) - .sort((a, b) => a - b) - .filter((d, i, a) => i === 0 || d !== a[i - 1]) - .reduce((acc, d, i, src) => (i === 0 || acc <= d - src[i - 1] ? acc : Math.abs(d - src[i - 1]))) - ); - - return Math.min(...gaps); - }; - - const getDataExtent = data => { - if (base.padUnit() == "domain") { - const dataWidth = getMinimumGap(data); - return base.pad([dataWidth / 2, dataWidth / 2])(data); - } else { - return base(data); - } - }; - - _domain.valueName = (...args) => { - if (!args.length) { - return valueNames[0]; - } - valueNames = [args[0]]; - return _domain; - }; - _domain.valueNames = (...args) => { - if (!args.length) { - return valueNames; - } - valueNames = args[0]; - return _domain; - }; - - return _domain; -}; - -export const labelFunction = valueName => d => d[valueName][0]; - -export const tickFormatFunction = valueformatter; +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ +import * as d3 from "d3"; +import * as fc from "d3fc"; +import {flattenArray} from "./flatten"; +import {extentLinear as customExtent} from "../d3fc/extent/extentLinear"; +import valueformatter from "./valueFormatter"; + +export const scale = () => d3.scaleLinear(); + +export const domain = () => { + const base = customExtent() + .pad([0, 0.1]) + .padUnit("percent"); + + let valueNames = ["crossValue"]; + + const _domain = data => { + base.accessors(valueNames.map(v => d => parseFloat(d[v]))); + + return getDataExtent(flattenArray(data)); + }; + + fc.rebindAll(_domain, base); + + const getMinimumGap = data => { + const gaps = valueNames.map(valueName => + data + .map(d => d[valueName]) + .sort((a, b) => a - b) + .filter((d, i, a) => i === 0 || d !== a[i - 1]) + .reduce((acc, d, i, src) => (i === 0 || acc <= d - src[i - 1] ? acc : Math.abs(d - src[i - 1]))) + ); + + return Math.min(...gaps); + }; + + const getDataExtent = data => { + if (base.padUnit() == "domain") { + const dataWidth = getMinimumGap(data); + return base.pad([dataWidth / 2, dataWidth / 2])(data); + } else { + return base(data); + } + }; + + _domain.valueName = (...args) => { + if (!args.length) { + return valueNames[0]; + } + valueNames = [args[0]]; + return _domain; + }; + _domain.valueNames = (...args) => { + if (!args.length) { + return valueNames; + } + valueNames = args[0]; + return _domain; + }; + + return _domain; +}; + +export const labelFunction = valueName => d => d[valueName][0]; + +export const tickFormatFunction = valueformatter; diff --git a/packages/perspective-viewer-d3fc/src/js/axis/minBandwidth.js b/packages/perspective-viewer-d3fc/src/js/axis/minBandwidth.js index c92a3035e7..4d9251e70d 100644 --- a/packages/perspective-viewer-d3fc/src/js/axis/minBandwidth.js +++ b/packages/perspective-viewer-d3fc/src/js/axis/minBandwidth.js @@ -1,29 +1,29 @@ -/****************************************************************************** - * - * Copyright (c) 2017, the Perspective Authors. - * - * This file is part of the Perspective library, distributed under the terms of - * the Apache License 2.0. The full license can be found in the LICENSE file. - * - */ -import {rebindAll} from "d3fc"; - -const MIN_BANDWIDTH = 1; - -export default adaptee => { - const minBandwidth = arg => { - return adaptee(arg); - }; - - rebindAll(minBandwidth, adaptee); - - minBandwidth.bandwidth = (...args) => { - if (!args.length) { - return Math.max(adaptee.bandwidth(), MIN_BANDWIDTH); - } - adaptee.bandwidth(...args); - return minBandwidth; - }; - - return minBandwidth; -}; +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ +import {rebindAll} from "d3fc"; + +const MIN_BANDWIDTH = 1; + +export default adaptee => { + const minBandwidth = arg => { + return adaptee(arg); + }; + + rebindAll(minBandwidth, adaptee); + + minBandwidth.bandwidth = (...args) => { + if (!args.length) { + return Math.max(adaptee.bandwidth(), MIN_BANDWIDTH); + } + adaptee.bandwidth(...args); + return minBandwidth; + }; + + return minBandwidth; +}; diff --git a/packages/perspective-viewer-d3fc/src/js/axis/noAxis.js b/packages/perspective-viewer-d3fc/src/js/axis/noAxis.js index 3dadf13d7d..f03b5efcc2 100644 --- a/packages/perspective-viewer-d3fc/src/js/axis/noAxis.js +++ b/packages/perspective-viewer-d3fc/src/js/axis/noAxis.js @@ -1,53 +1,53 @@ -/****************************************************************************** - * - * Copyright (c) 2017, the Perspective Authors. - * - * This file is part of the Perspective library, distributed under the terms of - * the Apache License 2.0. The full license can be found in the LICENSE file. - * - */ -import * as d3 from "d3"; -import {flattenArray} from "./flatten"; -import minBandwidth from "./minBandwidth"; -import withoutTicks from "./withoutTicks"; - -export const scale = () => withoutTicks(minBandwidth(d3.scaleBand())); - -export const domain = () => { - let valueNames = ["crossValue"]; - let orient = "horizontal"; - - const _domain = data => { - const flattenedData = flattenArray(data); - return transformDomain([...new Set(flattenedData.map(d => d[valueNames[0]]))]); - }; - - const transformDomain = d => (orient == "vertical" ? d.reverse() : d); - - _domain.valueName = (...args) => { - if (!args.length) { - return valueNames[0]; - } - valueNames = [args[0]]; - return _domain; - }; - _domain.valueNames = (...args) => { - if (!args.length) { - return valueNames; - } - valueNames = args[0]; - return _domain; - }; - - _domain.orient = (...args) => { - if (!args.length) { - return orient; - } - orient = args[0]; - return _domain; - }; - - return _domain; -}; - -export const labelFunction = valueName => d => d[valueName].join("|"); +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ +import * as d3 from "d3"; +import {flattenArray} from "./flatten"; +import minBandwidth from "./minBandwidth"; +import withoutTicks from "./withoutTicks"; + +export const scale = () => withoutTicks(minBandwidth(d3.scaleBand())); + +export const domain = () => { + let valueNames = ["crossValue"]; + let orient = "horizontal"; + + const _domain = data => { + const flattenedData = flattenArray(data); + return transformDomain([...new Set(flattenedData.map(d => d[valueNames[0]]))]); + }; + + const transformDomain = d => (orient == "vertical" ? d.reverse() : d); + + _domain.valueName = (...args) => { + if (!args.length) { + return valueNames[0]; + } + valueNames = [args[0]]; + return _domain; + }; + _domain.valueNames = (...args) => { + if (!args.length) { + return valueNames; + } + valueNames = args[0]; + return _domain; + }; + + _domain.orient = (...args) => { + if (!args.length) { + return orient; + } + orient = args[0]; + return _domain; + }; + + return _domain; +}; + +export const labelFunction = valueName => d => d[valueName].join("|"); diff --git a/packages/perspective-viewer-d3fc/src/js/axis/ordinalAxis.js b/packages/perspective-viewer-d3fc/src/js/axis/ordinalAxis.js index d3c059afb7..fa742d11a7 100644 --- a/packages/perspective-viewer-d3fc/src/js/axis/ordinalAxis.js +++ b/packages/perspective-viewer-d3fc/src/js/axis/ordinalAxis.js @@ -1,282 +1,282 @@ -/****************************************************************************** - * - * Copyright (c) 2017, the Perspective Authors. - * - * This file is part of the Perspective library, distributed under the terms of - * the Apache License 2.0. The full license can be found in the LICENSE file. - * - */ -import * as d3 from "d3"; -import * as fc from "d3fc"; -import minBandwidth from "./minBandwidth"; -import {flattenArray} from "./flatten"; -import {multiAxisBottom, multiAxisLeft, multiAxisTop, multiAxisRight} from "../d3fc/axis/multi-axis"; -import {getChartContainer} from "../plugin/root"; - -export const scale = () => minBandwidth(d3.scaleBand()).padding(0.5); - -export const domain = () => { - let valueNames = ["crossValue"]; - let orient = "horizontal"; - - const _domain = data => { - const flattenedData = flattenArray(data); - return transformDomain([...new Set(flattenedData.map(d => d[valueNames[0]]))]); - }; - - const transformDomain = d => (orient == "vertical" ? d.reverse() : d); - - _domain.valueName = (...args) => { - if (!args.length) { - return valueNames[0]; - } - valueNames = [args[0]]; - return _domain; - }; - _domain.valueNames = (...args) => { - if (!args.length) { - return valueNames; - } - valueNames = args[0]; - return _domain; - }; - - _domain.orient = (...args) => { - if (!args.length) { - return orient; - } - orient = args[0]; - return _domain; - }; - - return _domain; -}; - -export const labelFunction = valueName => d => d[valueName].join("|"); - -export const component = settings => { - let orient = "horizontal"; - let settingName = "crossValues"; - let domain = null; - - const getComponent = () => { - const multiLevel = settings[settingName].length > 1; - - // Calculate the label groups and corresponding group sizes - const levelGroups = axisGroups(domain); - const groupTickLayout = levelGroups.map(getGroupTickLayout); - - const tickSizeInner = multiLevel ? groupTickLayout.map(l => l.size) : groupTickLayout[0].size; - const tickSizeOuter = groupTickLayout.reduce((s, v) => s + v.size, 0); - - const createAxis = base => scale => { - const axis = base(scale); - - if (multiLevel) { - axis.groups(levelGroups) - .tickSizeInner(tickSizeInner) - .tickSizeOuter(tickSizeOuter); - } - if (orient !== "horizontal") axis.tickPadding(10); - return axis; - }; - - const decorate = (s, data, index) => { - const rotation = groupTickLayout[index].rotation; - if (orient === "horizontal") applyLabelRotation(s, rotation); - hideOverlappingLabels(s, rotation); - }; - - const axisSet = getAxisSet(multiLevel); - return { - bottom: createAxis(axisSet.bottom), - left: createAxis(axisSet.left), - right: createAxis(axisSet.right), - top: createAxis(axisSet.top), - size: `${tickSizeOuter + 10}px`, - decorate - }; - }; - - // const pickAxis = multiLevel => { - // if (multiLevel) { - // return orient === "horizontal" ? - // multiAxisBottom : multiAxisLeft; - // } - // return orient === "horizontal" ? - // fc.axisOrdinalBottom : fc.axisOrdinalLeft; - // }; - - const getAxisSet = multiLevel => { - if (multiLevel) { - return { - bottom: multiAxisBottom, - left: multiAxisLeft, - top: multiAxisTop, - right: multiAxisRight - }; - } else { - return { - bottom: fc.axisOrdinalBottom, - left: fc.axisOrdinalLeft, - top: fc.axisOrdinalTop, - right: fc.axisOrdinalRight - }; - } - }; - - const axisGroups = domain => { - const groups = []; - domain.forEach(tick => { - const split = tick && tick.split ? tick.split("|") : [tick]; - split.forEach((s, i) => { - while (groups.length <= i) groups.push([]); - - const group = groups[i]; - if (group.length > 0 && group[group.length - 1].text === s) { - group[group.length - 1].domain.push(tick); - } else { - group.push({text: s, domain: [tick]}); - } - }); - }); - return groups.reverse(); - }; - - const getGroupTickLayout = group => { - const width = settings.size.width; - const maxLength = Math.max(...group.map(g => (g.text ? g.text.length : 0))); - - if (orient === "horizontal") { - // x-axis may rotate labels and expand the available height - if (group && group.length * 16 > width - 100) { - return { - size: maxLength * 5 + 10, - rotation: 90 - }; - } else if (group && group.length * (maxLength * 6 + 10) > width - 100) { - return { - size: maxLength * 3 + 20, - rotation: 45 - }; - } - return { - size: 25, - rotation: 0 - }; - } else { - // y-axis size always based on label size - return { - size: maxLength * 5 + 10, - rotation: 0 - }; - } - }; - - const hideOverlappingLabels = (s, rotated) => { - const getTransformCoords = transform => { - const splitOn = transform.indexOf(",") !== -1 ? "," : " "; - const coords = transform - .substring(transform.indexOf("(") + 1, transform.indexOf(")")) - .split(splitOn) - .map(c => parseInt(c)); - while (coords.length < 2) coords.push(0); - return coords; - }; - - const rectanglesOverlap = (r1, r2) => r1.x <= r2.x + r2.width && r2.x <= r1.x + r1.width && r1.y <= r2.y + r2.height && r2.y <= r1.y + r1.height; - const rotatedLabelsOverlap = (r1, r2) => r1.x + r1.width + 14 > r2.x + r2.width; - const isOverlap = rotated ? rotatedLabelsOverlap : rectanglesOverlap; - - const rectangleContained = (r1, r2) => r1.x >= r2.x && r1.x + r1.width <= r2.x + r2.width && r1.y >= r2.y && r1.y + r1.height <= r2.y + r2.height; - // The bounds rect is the available screen space a label can fit into - const boundsRect = orient == "horizontal" ? getXAxisBoundsRect(s) : null; - - const previousRectangles = []; - s.each((d, i, nodes) => { - const tick = d3.select(nodes[i]); - - // How the "tick" element is transformed (x/y) - const transformCoords = getTransformCoords(tick.attr("transform")); - - // Work out the actual rectanble the label occupies - const tickRect = tick.node().getBBox(); - const rect = {x: tickRect.x + transformCoords[0], y: tickRect.y + transformCoords[1], width: tickRect.width, height: tickRect.height}; - - const overlap = previousRectangles.some(r => isOverlap(r, rect)); - - // Test that it also fits into the screen space - const hidden = overlap || (boundsRect && !rectangleContained(rect, boundsRect)); - - tick.attr("visibility", hidden ? "hidden" : ""); - if (!hidden) { - previousRectangles.push(rect); - } - }); - }; - - const getXAxisBoundsRect = s => { - const container = getChartContainer(s.node()); - if (container === null) { - return; - } - const chart = container.querySelector(".cartesian-chart"); - const axis = chart.querySelector(".x-axis"); - - const chartRect = chart.getBoundingClientRect(); - const axisRect = axis.getBoundingClientRect(); - return { - x: chartRect.left - axisRect.left, - width: chartRect.width, - y: chartRect.top - axisRect.top, - height: chartRect.height - }; - }; - - const getLabelTransform = rotation => { - if (!rotation) { - return "translate(0, 8)"; - } - if (rotation < 60) { - return `rotate(-${rotation} 5 5)`; - } - return `rotate(-${rotation} 3 7)`; - }; - - const applyLabelRotation = (s, rotation) => { - const transform = getLabelTransform(rotation); - const anchor = rotation ? "end" : ""; - s.each((d, i, nodes) => { - const tick = d3.select(nodes[i]); - const text = tick.select("text"); - - text.attr("transform", transform).style("text-anchor", anchor); - }); - }; - - getComponent.orient = (...args) => { - if (!args.length) { - return orient; - } - orient = args[0]; - return getComponent; - }; - - getComponent.settingName = (...args) => { - if (!args.length) { - return settingName; - } - settingName = args[0]; - return getComponent; - }; - - getComponent.domain = (...args) => { - if (!args.length) { - return domain; - } - domain = args[0]; - return getComponent; - }; - - return getComponent; -}; +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ +import * as d3 from "d3"; +import * as fc from "d3fc"; +import minBandwidth from "./minBandwidth"; +import {flattenArray} from "./flatten"; +import {multiAxisBottom, multiAxisLeft, multiAxisTop, multiAxisRight} from "../d3fc/axis/multi-axis"; +import {getChartContainer} from "../plugin/root"; + +export const scale = () => minBandwidth(d3.scaleBand()).padding(0.5); + +export const domain = () => { + let valueNames = ["crossValue"]; + let orient = "horizontal"; + + const _domain = data => { + const flattenedData = flattenArray(data); + return transformDomain([...new Set(flattenedData.map(d => d[valueNames[0]]))]); + }; + + const transformDomain = d => (orient == "vertical" ? d.reverse() : d); + + _domain.valueName = (...args) => { + if (!args.length) { + return valueNames[0]; + } + valueNames = [args[0]]; + return _domain; + }; + _domain.valueNames = (...args) => { + if (!args.length) { + return valueNames; + } + valueNames = args[0]; + return _domain; + }; + + _domain.orient = (...args) => { + if (!args.length) { + return orient; + } + orient = args[0]; + return _domain; + }; + + return _domain; +}; + +export const labelFunction = valueName => d => d[valueName].join("|"); + +export const component = settings => { + let orient = "horizontal"; + let settingName = "crossValues"; + let domain = null; + + const getComponent = () => { + const multiLevel = settings[settingName].length > 1; + + // Calculate the label groups and corresponding group sizes + const levelGroups = axisGroups(domain); + const groupTickLayout = levelGroups.map(getGroupTickLayout); + + const tickSizeInner = multiLevel ? groupTickLayout.map(l => l.size) : groupTickLayout[0].size; + const tickSizeOuter = groupTickLayout.reduce((s, v) => s + v.size, 0); + + const createAxis = base => scale => { + const axis = base(scale); + + if (multiLevel) { + axis.groups(levelGroups) + .tickSizeInner(tickSizeInner) + .tickSizeOuter(tickSizeOuter); + } + if (orient !== "horizontal") axis.tickPadding(10); + return axis; + }; + + const decorate = (s, data, index) => { + const rotation = groupTickLayout[index].rotation; + if (orient === "horizontal") applyLabelRotation(s, rotation); + hideOverlappingLabels(s, rotation); + }; + + const axisSet = getAxisSet(multiLevel); + return { + bottom: createAxis(axisSet.bottom), + left: createAxis(axisSet.left), + right: createAxis(axisSet.right), + top: createAxis(axisSet.top), + size: `${tickSizeOuter + 10}px`, + decorate + }; + }; + + // const pickAxis = multiLevel => { + // if (multiLevel) { + // return orient === "horizontal" ? + // multiAxisBottom : multiAxisLeft; + // } + // return orient === "horizontal" ? + // fc.axisOrdinalBottom : fc.axisOrdinalLeft; + // }; + + const getAxisSet = multiLevel => { + if (multiLevel) { + return { + bottom: multiAxisBottom, + left: multiAxisLeft, + top: multiAxisTop, + right: multiAxisRight + }; + } else { + return { + bottom: fc.axisOrdinalBottom, + left: fc.axisOrdinalLeft, + top: fc.axisOrdinalTop, + right: fc.axisOrdinalRight + }; + } + }; + + const axisGroups = domain => { + const groups = []; + domain.forEach(tick => { + const split = tick && tick.split ? tick.split("|") : [tick]; + split.forEach((s, i) => { + while (groups.length <= i) groups.push([]); + + const group = groups[i]; + if (group.length > 0 && group[group.length - 1].text === s) { + group[group.length - 1].domain.push(tick); + } else { + group.push({text: s, domain: [tick]}); + } + }); + }); + return groups.reverse(); + }; + + const getGroupTickLayout = group => { + const width = settings.size.width; + const maxLength = Math.max(...group.map(g => (g.text ? g.text.length : 0))); + + if (orient === "horizontal") { + // x-axis may rotate labels and expand the available height + if (group && group.length * 16 > width - 100) { + return { + size: maxLength * 5 + 10, + rotation: 90 + }; + } else if (group && group.length * (maxLength * 6 + 10) > width - 100) { + return { + size: maxLength * 3 + 20, + rotation: 45 + }; + } + return { + size: 25, + rotation: 0 + }; + } else { + // y-axis size always based on label size + return { + size: maxLength * 5 + 10, + rotation: 0 + }; + } + }; + + const hideOverlappingLabels = (s, rotated) => { + const getTransformCoords = transform => { + const splitOn = transform.indexOf(",") !== -1 ? "," : " "; + const coords = transform + .substring(transform.indexOf("(") + 1, transform.indexOf(")")) + .split(splitOn) + .map(c => parseInt(c)); + while (coords.length < 2) coords.push(0); + return coords; + }; + + const rectanglesOverlap = (r1, r2) => r1.x <= r2.x + r2.width && r2.x <= r1.x + r1.width && r1.y <= r2.y + r2.height && r2.y <= r1.y + r1.height; + const rotatedLabelsOverlap = (r1, r2) => r1.x + r1.width + 14 > r2.x + r2.width; + const isOverlap = rotated ? rotatedLabelsOverlap : rectanglesOverlap; + + const rectangleContained = (r1, r2) => r1.x >= r2.x && r1.x + r1.width <= r2.x + r2.width && r1.y >= r2.y && r1.y + r1.height <= r2.y + r2.height; + // The bounds rect is the available screen space a label can fit into + const boundsRect = orient == "horizontal" ? getXAxisBoundsRect(s) : null; + + const previousRectangles = []; + s.each((d, i, nodes) => { + const tick = d3.select(nodes[i]); + + // How the "tick" element is transformed (x/y) + const transformCoords = getTransformCoords(tick.attr("transform")); + + // Work out the actual rectanble the label occupies + const tickRect = tick.node().getBBox(); + const rect = {x: tickRect.x + transformCoords[0], y: tickRect.y + transformCoords[1], width: tickRect.width, height: tickRect.height}; + + const overlap = previousRectangles.some(r => isOverlap(r, rect)); + + // Test that it also fits into the screen space + const hidden = overlap || (boundsRect && !rectangleContained(rect, boundsRect)); + + tick.attr("visibility", hidden ? "hidden" : ""); + if (!hidden) { + previousRectangles.push(rect); + } + }); + }; + + const getXAxisBoundsRect = s => { + const container = getChartContainer(s.node()); + if (container === null) { + return; + } + const chart = container.querySelector(".cartesian-chart"); + const axis = chart.querySelector(".x-axis"); + + const chartRect = chart.getBoundingClientRect(); + const axisRect = axis.getBoundingClientRect(); + return { + x: chartRect.left - axisRect.left, + width: chartRect.width, + y: chartRect.top - axisRect.top, + height: chartRect.height + }; + }; + + const getLabelTransform = rotation => { + if (!rotation) { + return "translate(0, 8)"; + } + if (rotation < 60) { + return `rotate(-${rotation} 5 5)`; + } + return `rotate(-${rotation} 3 7)`; + }; + + const applyLabelRotation = (s, rotation) => { + const transform = getLabelTransform(rotation); + const anchor = rotation ? "end" : ""; + s.each((d, i, nodes) => { + const tick = d3.select(nodes[i]); + const text = tick.select("text"); + + text.attr("transform", transform).style("text-anchor", anchor); + }); + }; + + getComponent.orient = (...args) => { + if (!args.length) { + return orient; + } + orient = args[0]; + return getComponent; + }; + + getComponent.settingName = (...args) => { + if (!args.length) { + return settingName; + } + settingName = args[0]; + return getComponent; + }; + + getComponent.domain = (...args) => { + if (!args.length) { + return domain; + } + domain = args[0]; + return getComponent; + }; + + return getComponent; +}; diff --git a/packages/perspective-viewer-d3fc/src/js/axis/splitterLabels.js b/packages/perspective-viewer-d3fc/src/js/axis/splitterLabels.js index 1d6c7cea9a..8a676c9a66 100644 --- a/packages/perspective-viewer-d3fc/src/js/axis/splitterLabels.js +++ b/packages/perspective-viewer-d3fc/src/js/axis/splitterLabels.js @@ -1,73 +1,73 @@ -/****************************************************************************** - * - * Copyright (c) 2017, the Perspective Authors. - * - * This file is part of the Perspective library, distributed under the terms of - * the Apache License 2.0. The full license can be found in the LICENSE file. - * - */ -import * as fc from "d3fc"; -import {getChartElement} from "../plugin/root"; -import {withoutOpacity} from "../series/seriesColors.js"; - -// Render a set of labels with the little left/right arrows for moving -// between axes -export const splitterLabels = settings => { - let labels = []; - let alt = false; - let color; - - const _render = selection => { - selection.text(""); - - const labelDataJoin = fc.dataJoin("span", "splitter-label").key(d => d); - - const disabled = !alt && labels.length === 1; - const coloured = color && settings.splitValues.length === 0; - labelDataJoin(selection, labels) - .classed("disabled", disabled) - .text(d => d.name) - .style("color", d => (coloured ? withoutOpacity(color(d.name)) : undefined)) - .on("click", d => { - if (disabled) return; - - if (alt) { - settings.splitMainValues = settings.splitMainValues.filter(v => v != d.name); - } else { - settings.splitMainValues = [d.name].concat(settings.splitMainValues || []); - } - - redrawChart(selection); - }); - }; - - const redrawChart = selection => { - const chartElement = getChartElement(selection.node()); - chartElement.remove(); - chartElement.draw(); - }; - - _render.labels = (...args) => { - if (!args.length) { - return labels; - } - labels = args[0]; - return _render; - }; - _render.alt = (...args) => { - if (!args.length) { - return alt; - } - alt = args[0]; - return _render; - }; - - _render.color = (...args) => { - if (!args.length) { - return color; - } - color = args[0]; - return _render; - }; - return _render; -}; +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ +import * as fc from "d3fc"; +import {getChartElement} from "../plugin/root"; +import {withoutOpacity} from "../series/seriesColors.js"; + +// Render a set of labels with the little left/right arrows for moving +// between axes +export const splitterLabels = settings => { + let labels = []; + let alt = false; + let color; + + const _render = selection => { + selection.text(""); + + const labelDataJoin = fc.dataJoin("span", "splitter-label").key(d => d); + + const disabled = !alt && labels.length === 1; + const coloured = color && settings.splitValues.length === 0; + labelDataJoin(selection, labels) + .classed("disabled", disabled) + .text(d => d.name) + .style("color", d => (coloured ? withoutOpacity(color(d.name)) : undefined)) + .on("click", d => { + if (disabled) return; + + if (alt) { + settings.splitMainValues = settings.splitMainValues.filter(v => v != d.name); + } else { + settings.splitMainValues = [d.name].concat(settings.splitMainValues || []); + } + + redrawChart(selection); + }); + }; + + const redrawChart = selection => { + const chartElement = getChartElement(selection.node()); + chartElement.remove(); + chartElement.draw(); + }; + + _render.labels = (...args) => { + if (!args.length) { + return labels; + } + labels = args[0]; + return _render; + }; + _render.alt = (...args) => { + if (!args.length) { + return alt; + } + alt = args[0]; + return _render; + }; + + _render.color = (...args) => { + if (!args.length) { + return color; + } + color = args[0]; + return _render; + }; + return _render; +}; diff --git a/packages/perspective-viewer-d3fc/src/js/axis/timeAxis.js b/packages/perspective-viewer-d3fc/src/js/axis/timeAxis.js index 916efc7aa0..0aa7e5b35a 100644 --- a/packages/perspective-viewer-d3fc/src/js/axis/timeAxis.js +++ b/packages/perspective-viewer-d3fc/src/js/axis/timeAxis.js @@ -1,63 +1,63 @@ -/****************************************************************************** - * - * Copyright (c) 2017, the Perspective Authors. - * - * This file is part of the Perspective library, distributed under the terms of - * the Apache License 2.0. The full license can be found in the LICENSE file. - * - */ -import * as d3 from "d3"; -import * as fc from "d3fc"; -import {flattenArray} from "./flatten"; - -export const scale = () => d3.scaleTime(); - -export const domain = () => { - const base = fc.extentTime(); - - let valueNames = ["crossValue"]; - - const _domain = data => { - base.accessors(valueNames.map(v => d => new Date(d[v]))); - - return getDataExtent(flattenArray(data)); - }; - - fc.rebindAll(_domain, base, fc.exclude("include", "paddingStrategy")); - - const getMinimumGap = data => { - const gaps = valueNames.map(valueName => - data - .map(d => new Date(d[valueName]).getTime()) - .sort((a, b) => a - b) - .filter((d, i, a) => i === 0 || d !== a[i - 1]) - .reduce((acc, d, i, src) => (i === 0 || acc <= d - src[i - 1] ? acc : Math.abs(d - src[i - 1]))) - ); - - return Math.min(...gaps); - }; - - const getDataExtent = data => { - const dataWidth = getMinimumGap(data); - return base.padUnit("domain").pad([dataWidth / 2, dataWidth / 2])(data); - }; - - _domain.valueName = (...args) => { - if (!args.length) { - return valueNames[0]; - } - valueNames = [args[0]]; - return _domain; - }; - _domain.valueNames = (...args) => { - if (!args.length) { - return valueNames; - } - valueNames = args[0]; - return _domain; - }; - - return _domain; -}; - -export const labelFunction = valueName => d => new Date(d[valueName][0]); +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ +import * as d3 from "d3"; +import * as fc from "d3fc"; +import {flattenArray} from "./flatten"; + +export const scale = () => d3.scaleTime(); + +export const domain = () => { + const base = fc.extentTime(); + + let valueNames = ["crossValue"]; + + const _domain = data => { + base.accessors(valueNames.map(v => d => new Date(d[v]))); + + return getDataExtent(flattenArray(data)); + }; + + fc.rebindAll(_domain, base, fc.exclude("include", "paddingStrategy")); + + const getMinimumGap = data => { + const gaps = valueNames.map(valueName => + data + .map(d => new Date(d[valueName]).getTime()) + .sort((a, b) => a - b) + .filter((d, i, a) => i === 0 || d !== a[i - 1]) + .reduce((acc, d, i, src) => (i === 0 || acc <= d - src[i - 1] ? acc : Math.abs(d - src[i - 1]))) + ); + + return Math.min(...gaps); + }; + + const getDataExtent = data => { + const dataWidth = getMinimumGap(data); + return base.padUnit("domain").pad([dataWidth / 2, dataWidth / 2])(data); + }; + + _domain.valueName = (...args) => { + if (!args.length) { + return valueNames[0]; + } + valueNames = [args[0]]; + return _domain; + }; + _domain.valueNames = (...args) => { + if (!args.length) { + return valueNames; + } + valueNames = args[0]; + return _domain; + }; + + return _domain; +}; + +export const labelFunction = valueName => d => new Date(d[valueName][0]); diff --git a/packages/perspective-viewer-d3fc/src/js/axis/withoutTicks.js b/packages/perspective-viewer-d3fc/src/js/axis/withoutTicks.js index fade12545f..c58acd7a97 100644 --- a/packages/perspective-viewer-d3fc/src/js/axis/withoutTicks.js +++ b/packages/perspective-viewer-d3fc/src/js/axis/withoutTicks.js @@ -1,23 +1,23 @@ -/****************************************************************************** - * - * Copyright (c) 2017, the Perspective Authors. - * - * This file is part of the Perspective library, distributed under the terms of - * the Apache License 2.0. The full license can be found in the LICENSE file. - * - */ -import {rebindAll} from "d3fc"; - -export default adaptee => { - const withoutTicks = arg => { - return adaptee(arg); - }; - - rebindAll(withoutTicks, adaptee); - - withoutTicks.ticks = function() { - return []; - }; - - return withoutTicks; -}; +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ +import {rebindAll} from "d3fc"; + +export default adaptee => { + const withoutTicks = arg => { + return adaptee(arg); + }; + + rebindAll(withoutTicks, adaptee); + + withoutTicks.ticks = function() { + return []; + }; + + return withoutTicks; +}; diff --git a/packages/perspective-viewer-d3fc/src/js/charts/area.js b/packages/perspective-viewer-d3fc/src/js/charts/area.js index 0b5419b42e..4574283848 100644 --- a/packages/perspective-viewer-d3fc/src/js/charts/area.js +++ b/packages/perspective-viewer-d3fc/src/js/charts/area.js @@ -1,94 +1,94 @@ -/****************************************************************************** - * - * Copyright (c) 2017, the Perspective Authors. - * - * This file is part of the Perspective library, distributed under the terms of - * the Apache License 2.0. The full license can be found in the LICENSE file. - * - */ -import * as fc from "d3fc"; -import {axisFactory} from "../axis/axisFactory"; -import {chartSvgFactory} from "../axis/chartFactory"; -import {axisSplitter} from "../axis/axisSplitter"; -import {AXIS_TYPES} from "../axis/axisType"; -import {areaSeries} from "../series/areaSeries"; -import {seriesColors} from "../series/seriesColors"; -import {splitAndBaseData} from "../data/splitAndBaseData"; -import {colorLegend} from "../legend/legend"; -import {filterData} from "../legend/filter"; -import withGridLines from "../gridlines/gridlines"; - -import {hardLimitZeroPadding} from "../d3fc/padding/hardLimitZero"; -import zoomableChart from "../zoom/zoomableChart"; -import nearbyTip from "../tooltip/nearbyTip"; - -function areaChart(container, settings) { - const data = splitAndBaseData(settings, filterData(settings)); - - const color = seriesColors(settings); - const legend = colorLegend() - .settings(settings) - .scale(color); - - const series = fc.seriesSvgRepeat().series(areaSeries(settings, color).orient("vertical")); - - const xAxis = axisFactory(settings) - .excludeType(AXIS_TYPES.linear) - .settingName("crossValues") - .valueName("crossValue")(data); - const yAxisFactory = axisFactory(settings) - .settingName("mainValues") - .valueName("mainValue") - .excludeType(AXIS_TYPES.ordinal) - .orient("vertical") - .include([0]) - .paddingStrategy(hardLimitZeroPadding()); - - // Check whether we've split some values into a second y-axis - const splitter = axisSplitter(settings, data).color(color); - - const yAxis1 = yAxisFactory(splitter.data()); - - // No grid lines if splitting y-axis - const plotSeries = splitter.haveSplit() ? series : withGridLines(series, settings).orient("vertical"); - - const chart = chartSvgFactory(xAxis, yAxis1) - .axisSplitter(splitter) - .plotArea(plotSeries); - - chart.yNice && chart.yNice(); - - const zoomChart = zoomableChart() - .chart(chart) - .settings(settings) - .xScale(xAxis.scale); - - const toolTip = nearbyTip() - .settings(settings) - .xScale(xAxis.scale) - .yScale(yAxis1.scale) - .color(color) - .data(data); - - if (splitter.haveSplit()) { - // Create the y-axis data for the alt-axis - const yAxis2 = yAxisFactory(splitter.altData()); - chart.altAxis(yAxis2); - // Give the tooltip the information (i.e. 2 datasets with different - // scales) - toolTip.data(splitter.data()).altDataWithScale({yScale: yAxis2.scale, data: splitter.altData()}); - } - - // render - container.datum(splitter.data()).call(zoomChart); - container.call(toolTip); - container.call(legend); -} -areaChart.plugin = { - type: "d3_y_area", - name: "Y Area Chart", - max_cells: 4000, - max_columns: 50 -}; - -export default areaChart; +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ +import * as fc from "d3fc"; +import {axisFactory} from "../axis/axisFactory"; +import {chartSvgFactory} from "../axis/chartFactory"; +import {axisSplitter} from "../axis/axisSplitter"; +import {AXIS_TYPES} from "../axis/axisType"; +import {areaSeries} from "../series/areaSeries"; +import {seriesColors} from "../series/seriesColors"; +import {splitAndBaseData} from "../data/splitAndBaseData"; +import {colorLegend} from "../legend/legend"; +import {filterData} from "../legend/filter"; +import withGridLines from "../gridlines/gridlines"; + +import {hardLimitZeroPadding} from "../d3fc/padding/hardLimitZero"; +import zoomableChart from "../zoom/zoomableChart"; +import nearbyTip from "../tooltip/nearbyTip"; + +function areaChart(container, settings) { + const data = splitAndBaseData(settings, filterData(settings)); + + const color = seriesColors(settings); + const legend = colorLegend() + .settings(settings) + .scale(color); + + const series = fc.seriesSvgRepeat().series(areaSeries(settings, color).orient("vertical")); + + const xAxis = axisFactory(settings) + .excludeType(AXIS_TYPES.linear) + .settingName("crossValues") + .valueName("crossValue")(data); + const yAxisFactory = axisFactory(settings) + .settingName("mainValues") + .valueName("mainValue") + .excludeType(AXIS_TYPES.ordinal) + .orient("vertical") + .include([0]) + .paddingStrategy(hardLimitZeroPadding()); + + // Check whether we've split some values into a second y-axis + const splitter = axisSplitter(settings, data).color(color); + + const yAxis1 = yAxisFactory(splitter.data()); + + // No grid lines if splitting y-axis + const plotSeries = splitter.haveSplit() ? series : withGridLines(series, settings).orient("vertical"); + + const chart = chartSvgFactory(xAxis, yAxis1) + .axisSplitter(splitter) + .plotArea(plotSeries); + + chart.yNice && chart.yNice(); + + const zoomChart = zoomableChart() + .chart(chart) + .settings(settings) + .xScale(xAxis.scale); + + const toolTip = nearbyTip() + .settings(settings) + .xScale(xAxis.scale) + .yScale(yAxis1.scale) + .color(color) + .data(data); + + if (splitter.haveSplit()) { + // Create the y-axis data for the alt-axis + const yAxis2 = yAxisFactory(splitter.altData()); + chart.altAxis(yAxis2); + // Give the tooltip the information (i.e. 2 datasets with different + // scales) + toolTip.data(splitter.data()).altDataWithScale({yScale: yAxis2.scale, data: splitter.altData()}); + } + + // render + container.datum(splitter.data()).call(zoomChart); + container.call(toolTip); + container.call(legend); +} +areaChart.plugin = { + type: "d3_y_area", + name: "Y Area Chart", + max_cells: 4000, + max_columns: 50 +}; + +export default areaChart; diff --git a/packages/perspective-viewer-d3fc/src/js/charts/bar.js b/packages/perspective-viewer-d3fc/src/js/charts/bar.js index b32c101633..4327872e50 100644 --- a/packages/perspective-viewer-d3fc/src/js/charts/bar.js +++ b/packages/perspective-viewer-d3fc/src/js/charts/bar.js @@ -1,74 +1,74 @@ -/****************************************************************************** - * - * Copyright (c) 2017, the Perspective Authors. - * - * This file is part of the Perspective library, distributed under the terms of - * the Apache License 2.0. The full license can be found in the LICENSE file. - * - */ -import * as fc from "d3fc"; -import {axisFactory} from "../axis/axisFactory"; -import {chartSvgFactory} from "../axis/chartFactory"; -import {AXIS_TYPES} from "../axis/axisType"; -import {barSeries} from "../series/barSeries"; -import {seriesColors} from "../series/seriesColors"; -import {groupAndStackData} from "../data/groupData"; -import {colorLegend} from "../legend/legend"; -import {filterData} from "../legend/filter"; -import withGridLines from "../gridlines/gridlines"; - -import {hardLimitZeroPadding} from "../d3fc/padding/hardLimitZero"; -import zoomableChart from "../zoom/zoomableChart"; - -function barChart(container, settings) { - const data = groupAndStackData(settings, filterData(settings)); - const color = seriesColors(settings); - - const legend = colorLegend() - .settings(settings) - .scale(color); - - const bars = barSeries(settings, color).orient("horizontal"); - const series = fc - .seriesSvgMulti() - .mapping((data, index) => data[index]) - .series(data.map(() => bars)); - - const xAxis = axisFactory(settings) - .settingName("mainValues") - .valueName("mainValue") - .excludeType(AXIS_TYPES.ordinal) - .include([0]) - .paddingStrategy(hardLimitZeroPadding())(data); - const yAxis = axisFactory(settings) - .excludeType(AXIS_TYPES.linear) - .settingName("crossValues") - .valueName("crossValue") - .orient("vertical")(data); - - const chart = chartSvgFactory(xAxis, yAxis).plotArea(withGridLines(series, settings).orient("horizontal")); - - if (chart.yPaddingInner) { - chart.yPaddingInner(0.5); - chart.yPaddingOuter(0.25); - bars.align("left"); - } - chart.xNice && chart.xNice(); - - const zoomChart = zoomableChart() - .chart(chart) - .settings(settings) - .yScale(yAxis.scale); - - // render - container.datum(data).call(zoomChart); - container.call(legend); -} -barChart.plugin = { - type: "d3_x_bar", - name: "X Bar Chart", - max_cells: 1000, - max_columns: 50 -}; - -export default barChart; +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ +import * as fc from "d3fc"; +import {axisFactory} from "../axis/axisFactory"; +import {chartSvgFactory} from "../axis/chartFactory"; +import {AXIS_TYPES} from "../axis/axisType"; +import {barSeries} from "../series/barSeries"; +import {seriesColors} from "../series/seriesColors"; +import {groupAndStackData} from "../data/groupData"; +import {colorLegend} from "../legend/legend"; +import {filterData} from "../legend/filter"; +import withGridLines from "../gridlines/gridlines"; + +import {hardLimitZeroPadding} from "../d3fc/padding/hardLimitZero"; +import zoomableChart from "../zoom/zoomableChart"; + +function barChart(container, settings) { + const data = groupAndStackData(settings, filterData(settings)); + const color = seriesColors(settings); + + const legend = colorLegend() + .settings(settings) + .scale(color); + + const bars = barSeries(settings, color).orient("horizontal"); + const series = fc + .seriesSvgMulti() + .mapping((data, index) => data[index]) + .series(data.map(() => bars)); + + const xAxis = axisFactory(settings) + .settingName("mainValues") + .valueName("mainValue") + .excludeType(AXIS_TYPES.ordinal) + .include([0]) + .paddingStrategy(hardLimitZeroPadding())(data); + const yAxis = axisFactory(settings) + .excludeType(AXIS_TYPES.linear) + .settingName("crossValues") + .valueName("crossValue") + .orient("vertical")(data); + + const chart = chartSvgFactory(xAxis, yAxis).plotArea(withGridLines(series, settings).orient("horizontal")); + + if (chart.yPaddingInner) { + chart.yPaddingInner(0.5); + chart.yPaddingOuter(0.25); + bars.align("left"); + } + chart.xNice && chart.xNice(); + + const zoomChart = zoomableChart() + .chart(chart) + .settings(settings) + .yScale(yAxis.scale); + + // render + container.datum(data).call(zoomChart); + container.call(legend); +} +barChart.plugin = { + type: "d3_x_bar", + name: "X Bar Chart", + max_cells: 1000, + max_columns: 50 +}; + +export default barChart; diff --git a/packages/perspective-viewer-d3fc/src/js/charts/charts.js b/packages/perspective-viewer-d3fc/src/js/charts/charts.js index 0d1450ef20..9480698fbb 100644 --- a/packages/perspective-viewer-d3fc/src/js/charts/charts.js +++ b/packages/perspective-viewer-d3fc/src/js/charts/charts.js @@ -1,24 +1,24 @@ -/****************************************************************************** - * - * Copyright (c) 2017, the Perspective Authors. - * - * This file is part of the Perspective library, distributed under the terms of - * the Apache License 2.0. The full license can be found in the LICENSE file. - * - */ - -import barChart from "./bar"; -import columnChart from "./column"; -import lineChart from "./line"; -import areaChart from "./area"; -import yScatter from "./y-scatter"; -import xyScatter from "./xy-scatter"; -import heatmap from "./heatmap"; -import ohlc from "./ohlc"; -import candlestick from "./candlestick"; -import sunburst from "./sunburst"; -import treemap from "./treemap"; - -const chartClasses = [barChart, columnChart, lineChart, areaChart, yScatter, xyScatter, heatmap, ohlc, candlestick, sunburst, treemap]; - -export default chartClasses; +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ + +import barChart from "./bar"; +import columnChart from "./column"; +import lineChart from "./line"; +import areaChart from "./area"; +import yScatter from "./y-scatter"; +import xyScatter from "./xy-scatter"; +import heatmap from "./heatmap"; +import ohlc from "./ohlc"; +import candlestick from "./candlestick"; +import sunburst from "./sunburst"; +import treemap from "./treemap"; + +const chartClasses = [barChart, columnChart, lineChart, areaChart, yScatter, xyScatter, heatmap, ohlc, candlestick, sunburst, treemap]; + +export default chartClasses; diff --git a/packages/perspective-viewer-d3fc/src/js/charts/column.js b/packages/perspective-viewer-d3fc/src/js/charts/column.js index 2d62291812..8a6a8a4ef1 100644 --- a/packages/perspective-viewer-d3fc/src/js/charts/column.js +++ b/packages/perspective-viewer-d3fc/src/js/charts/column.js @@ -1,94 +1,94 @@ -/****************************************************************************** - * - * Copyright (c) 2017, the Perspective Authors. - * - * This file is part of the Perspective library, distributed under the terms of - * the Apache License 2.0. The full license can be found in the LICENSE file. - * - */ -import * as fc from "d3fc"; -import {axisFactory} from "../axis/axisFactory"; -import {chartSvgFactory} from "../axis/chartFactory"; -import domainMatchOrigins from "../axis/domainMatchOrigins"; -import {axisSplitter, dataBlankFunction, groupedBlankFunction} from "../axis/axisSplitter"; -import {AXIS_TYPES} from "../axis/axisType"; -import {barSeries} from "../series/barSeries"; -import {seriesColors} from "../series/seriesColors"; -import {groupAndStackData} from "../data/groupData"; -import {colorLegend} from "../legend/legend"; -import {filterData} from "../legend/filter"; -import withGridLines from "../gridlines/gridlines"; -import {hardLimitZeroPadding} from "../d3fc/padding/hardLimitZero"; -import zoomableChart from "../zoom/zoomableChart"; - -function columnChart(container, settings) { - const data = groupAndStackData(settings, filterData(settings)); - const color = seriesColors(settings); - - const legend = colorLegend() - .settings(settings) - .scale(color); - - const bars = barSeries(settings, color).orient("vertical"); - const series = fc - .seriesSvgMulti() - .mapping((data, index) => data[index]) - .series(data.map(() => bars)); - - const xAxis = axisFactory(settings) - .excludeType(AXIS_TYPES.linear) - .settingName("crossValues") - .valueName("crossValue")(data); - const yAxisFactory = axisFactory(settings) - .settingName("mainValues") - .valueName("mainValue") - .excludeType(AXIS_TYPES.ordinal) - .orient("vertical") - .include([0]) - .paddingStrategy(hardLimitZeroPadding()); - - // Check whether we've split some values into a second y-axis - const blankFunction = settings.mainValues.length > 1 ? groupedBlankFunction : dataBlankFunction; - const splitter = axisSplitter(settings, data, blankFunction).color(color); - - const yAxis1 = yAxisFactory(splitter.data()); - - // No grid lines if splitting y-axis - const plotSeries = splitter.haveSplit() ? series : withGridLines(series, settings).orient("vertical"); - - const chart = chartSvgFactory(xAxis, yAxis1) - .axisSplitter(splitter) - .plotArea(plotSeries); - - if (chart.xPaddingInner) { - chart.xPaddingInner(0.5); - chart.xPaddingOuter(0.25); - bars.align("left"); - } - chart.yNice && chart.yNice(); - - const zoomChart = zoomableChart() - .chart(chart) - .settings(settings) - .xScale(xAxis.scale); - - if (splitter.haveSplit()) { - // Create the y-axis data for the alt-axis - const yAxis2 = yAxisFactory(splitter.altData()); - - domainMatchOrigins(yAxis1.domain, yAxis2.domain); - chart.yDomain(yAxis1.domain).altAxis(yAxis2); - } - - // render - container.datum(splitter.data()).call(zoomChart); - container.call(legend); -} -columnChart.plugin = { - type: "d3_y_bar", - name: "Y Bar Chart", - max_cells: 1000, - max_columns: 50 -}; - -export default columnChart; +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ +import * as fc from "d3fc"; +import {axisFactory} from "../axis/axisFactory"; +import {chartSvgFactory} from "../axis/chartFactory"; +import domainMatchOrigins from "../axis/domainMatchOrigins"; +import {axisSplitter, dataBlankFunction, groupedBlankFunction} from "../axis/axisSplitter"; +import {AXIS_TYPES} from "../axis/axisType"; +import {barSeries} from "../series/barSeries"; +import {seriesColors} from "../series/seriesColors"; +import {groupAndStackData} from "../data/groupData"; +import {colorLegend} from "../legend/legend"; +import {filterData} from "../legend/filter"; +import withGridLines from "../gridlines/gridlines"; +import {hardLimitZeroPadding} from "../d3fc/padding/hardLimitZero"; +import zoomableChart from "../zoom/zoomableChart"; + +function columnChart(container, settings) { + const data = groupAndStackData(settings, filterData(settings)); + const color = seriesColors(settings); + + const legend = colorLegend() + .settings(settings) + .scale(color); + + const bars = barSeries(settings, color).orient("vertical"); + const series = fc + .seriesSvgMulti() + .mapping((data, index) => data[index]) + .series(data.map(() => bars)); + + const xAxis = axisFactory(settings) + .excludeType(AXIS_TYPES.linear) + .settingName("crossValues") + .valueName("crossValue")(data); + const yAxisFactory = axisFactory(settings) + .settingName("mainValues") + .valueName("mainValue") + .excludeType(AXIS_TYPES.ordinal) + .orient("vertical") + .include([0]) + .paddingStrategy(hardLimitZeroPadding()); + + // Check whether we've split some values into a second y-axis + const blankFunction = settings.mainValues.length > 1 ? groupedBlankFunction : dataBlankFunction; + const splitter = axisSplitter(settings, data, blankFunction).color(color); + + const yAxis1 = yAxisFactory(splitter.data()); + + // No grid lines if splitting y-axis + const plotSeries = splitter.haveSplit() ? series : withGridLines(series, settings).orient("vertical"); + + const chart = chartSvgFactory(xAxis, yAxis1) + .axisSplitter(splitter) + .plotArea(plotSeries); + + if (chart.xPaddingInner) { + chart.xPaddingInner(0.5); + chart.xPaddingOuter(0.25); + bars.align("left"); + } + chart.yNice && chart.yNice(); + + const zoomChart = zoomableChart() + .chart(chart) + .settings(settings) + .xScale(xAxis.scale); + + if (splitter.haveSplit()) { + // Create the y-axis data for the alt-axis + const yAxis2 = yAxisFactory(splitter.altData()); + + domainMatchOrigins(yAxis1.domain, yAxis2.domain); + chart.yDomain(yAxis1.domain).altAxis(yAxis2); + } + + // render + container.datum(splitter.data()).call(zoomChart); + container.call(legend); +} +columnChart.plugin = { + type: "d3_y_bar", + name: "Y Bar Chart", + max_cells: 1000, + max_columns: 50 +}; + +export default columnChart; diff --git a/packages/perspective-viewer-d3fc/src/js/charts/line.js b/packages/perspective-viewer-d3fc/src/js/charts/line.js index d1dcf235b4..e2374f0eff 100644 --- a/packages/perspective-viewer-d3fc/src/js/charts/line.js +++ b/packages/perspective-viewer-d3fc/src/js/charts/line.js @@ -1,103 +1,103 @@ -/****************************************************************************** - * - * Copyright (c) 2017, the Perspective Authors. - * - * This file is part of the Perspective library, distributed under the terms of - * the Apache License 2.0. The full license can be found in the LICENSE file. - * - */ -import * as fc from "d3fc"; -import {axisFactory} from "../axis/axisFactory"; -import {AXIS_TYPES} from "../axis/axisType"; -import {chartSvgFactory} from "../axis/chartFactory"; -import {axisSplitter} from "../axis/axisSplitter"; -import {seriesColors} from "../series/seriesColors"; -import {lineSeries} from "../series/lineSeries"; -import {splitData} from "../data/splitData"; -import {colorLegend} from "../legend/legend"; -import {filterData} from "../legend/filter"; -import {transposeData} from "../data/transposeData"; -import withGridLines from "../gridlines/gridlines"; - -import {hardLimitZeroPadding} from "../d3fc/padding/hardLimitZero"; -import zoomableChart from "../zoom/zoomableChart"; -import nearbyTip from "../tooltip/nearbyTip"; - -function lineChart(container, settings) { - const data = splitData(settings, filterData(settings)); - const color = seriesColors(settings); - - const legend = colorLegend() - .settings(settings) - .scale(color); - - const series = fc - .seriesSvgRepeat() - .series(lineSeries(settings, color)) - .orient("horizontal"); - - const paddingStrategy = hardLimitZeroPadding() - .pad([0.1, 0.1]) - .padUnit("percent"); - - const xAxis = axisFactory(settings) - .excludeType(AXIS_TYPES.linear) - .settingName("crossValues") - .valueName("crossValue")(data); - - const yAxisFactory = axisFactory(settings) - .settingName("mainValues") - .valueName("mainValue") - .orient("vertical") - .paddingStrategy(paddingStrategy); - - // Check whether we've split some values into a second y-axis - const splitter = axisSplitter(settings, transposeData(data)).color(color); - - const yAxis1 = yAxisFactory(splitter.data()); - - // No grid lines if splitting y-axis - const plotSeries = splitter.haveSplit() ? series : withGridLines(series, settings).orient("vertical"); - const chart = chartSvgFactory(xAxis, yAxis1) - .axisSplitter(splitter) - .plotArea(plotSeries); - - chart.yNice && chart.yNice(); - - const zoomChart = zoomableChart() - .chart(chart) - .settings(settings) - .xScale(xAxis.scale); - - const toolTip = nearbyTip() - .settings(settings) - .xScale(xAxis.scale) - .yScale(yAxis1.scale) - .color(color) - .data(data); - - if (splitter.haveSplit()) { - // Create the y-axis data for the alt-axis - const yAxis2 = yAxisFactory(splitter.altData()); - chart.altAxis(yAxis2); - // Give the tooltip the information (i.e. 2 datasets with different - // scales) - toolTip.data(splitter.data()).altDataWithScale({yScale: yAxis2.scale, data: splitter.altData()}); - } - - const transposed_data = splitter.data(); - - // render - container.datum(transposed_data).call(zoomChart); - container.call(toolTip); - container.call(legend); -} - -lineChart.plugin = { - type: "d3_y_line", - name: "Y Line Chart", - max_cells: 4000, - max_columns: 50 -}; - -export default lineChart; +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ +import * as fc from "d3fc"; +import {axisFactory} from "../axis/axisFactory"; +import {AXIS_TYPES} from "../axis/axisType"; +import {chartSvgFactory} from "../axis/chartFactory"; +import {axisSplitter} from "../axis/axisSplitter"; +import {seriesColors} from "../series/seriesColors"; +import {lineSeries} from "../series/lineSeries"; +import {splitData} from "../data/splitData"; +import {colorLegend} from "../legend/legend"; +import {filterData} from "../legend/filter"; +import {transposeData} from "../data/transposeData"; +import withGridLines from "../gridlines/gridlines"; + +import {hardLimitZeroPadding} from "../d3fc/padding/hardLimitZero"; +import zoomableChart from "../zoom/zoomableChart"; +import nearbyTip from "../tooltip/nearbyTip"; + +function lineChart(container, settings) { + const data = splitData(settings, filterData(settings)); + const color = seriesColors(settings); + + const legend = colorLegend() + .settings(settings) + .scale(color); + + const series = fc + .seriesSvgRepeat() + .series(lineSeries(settings, color)) + .orient("horizontal"); + + const paddingStrategy = hardLimitZeroPadding() + .pad([0.1, 0.1]) + .padUnit("percent"); + + const xAxis = axisFactory(settings) + .excludeType(AXIS_TYPES.linear) + .settingName("crossValues") + .valueName("crossValue")(data); + + const yAxisFactory = axisFactory(settings) + .settingName("mainValues") + .valueName("mainValue") + .orient("vertical") + .paddingStrategy(paddingStrategy); + + // Check whether we've split some values into a second y-axis + const splitter = axisSplitter(settings, transposeData(data)).color(color); + + const yAxis1 = yAxisFactory(splitter.data()); + + // No grid lines if splitting y-axis + const plotSeries = splitter.haveSplit() ? series : withGridLines(series, settings).orient("vertical"); + const chart = chartSvgFactory(xAxis, yAxis1) + .axisSplitter(splitter) + .plotArea(plotSeries); + + chart.yNice && chart.yNice(); + + const zoomChart = zoomableChart() + .chart(chart) + .settings(settings) + .xScale(xAxis.scale); + + const toolTip = nearbyTip() + .settings(settings) + .xScale(xAxis.scale) + .yScale(yAxis1.scale) + .color(color) + .data(data); + + if (splitter.haveSplit()) { + // Create the y-axis data for the alt-axis + const yAxis2 = yAxisFactory(splitter.altData()); + chart.altAxis(yAxis2); + // Give the tooltip the information (i.e. 2 datasets with different + // scales) + toolTip.data(splitter.data()).altDataWithScale({yScale: yAxis2.scale, data: splitter.altData()}); + } + + const transposed_data = splitter.data(); + + // render + container.datum(transposed_data).call(zoomChart); + container.call(toolTip); + container.call(legend); +} + +lineChart.plugin = { + type: "d3_y_line", + name: "Y Line Chart", + max_cells: 4000, + max_columns: 50 +}; + +export default lineChart; diff --git a/packages/perspective-viewer-d3fc/src/js/d3fc/axis/multi-axis.js b/packages/perspective-viewer-d3fc/src/js/d3fc/axis/multi-axis.js index 68da5babd3..becb74986d 100644 --- a/packages/perspective-viewer-d3fc/src/js/d3fc/axis/multi-axis.js +++ b/packages/perspective-viewer-d3fc/src/js/d3fc/axis/multi-axis.js @@ -1,154 +1,154 @@ -import {select, line} from "d3"; -import {axisOrdinalTop, axisOrdinalBottom, axisOrdinalLeft, axisOrdinalRight, dataJoin, rebindAll, exclude} from "d3fc"; -import store from "./store"; - -const multiAxis = (orient, baseAxis, scale) => { - let tickSizeOuter = 6; - let tickSizeInner = 6; - let axisStore = store("tickFormat", "ticks", "tickArguments", "tickValues", "tickPadding"); - let decorate = () => {}; - - let groups = null; - - const groupDataJoin = dataJoin("g", "group"); - const domainPathDataJoin = dataJoin("path", "domain"); - - const translate = (x, y) => (isVertical() ? `translate(${y}, ${x})` : `translate(${x}, ${y})`); - - const pathTranspose = arr => (isVertical() ? arr.map(d => [d[1], d[0]]) : arr); - - const isVertical = () => orient === "left" || orient === "right"; - - const multiAxis = selection => { - if (!groups) { - axisStore(baseAxis(scale).decorate(decorate))(selection); - return; - } - - if (selection.selection) { - groupDataJoin.transition(selection); - domainPathDataJoin.transition(selection); - } - - selection.each((data, index, group) => { - const element = group[index]; - - const container = select(element); - - const sign = orient === "bottom" || orient === "right" ? 1 : -1; - - // add the domain line - const range = scale.range(); - const domainPathData = pathTranspose([ - [range[0], sign * tickSizeOuter], - [range[0], 0], - [range[1], 0], - [range[1], sign * tickSizeOuter] - ]); - - const domainLine = domainPathDataJoin(container, [data]); - domainLine - .attr("d", line()(domainPathData)) - .attr("stroke", "#000") - .attr("fill", "none"); - - const g = groupDataJoin(container, groups); - - const getAxisSize = i => (Array.isArray(tickSizeInner) ? tickSizeInner[i] : tickSizeInner); - const getAxisOffset = i => { - let sum = 0; - for (let n = 0; n < i; n++) { - sum += getAxisSize(n); - } - return sum; - }; - - g.attr("transform", (d, i) => translate(0, sign * getAxisOffset(i))).each((group, i, nodes) => { - const groupElement = select(nodes[i]); - const groupScale = scaleFromGroup(scale, group); - const useAxis = axisStore(baseAxis(groupScale)) - .decorate((s, data) => decorate(s, data, i)) - .tickSizeInner(getAxisSize(i)) - .tickOffset(d => groupScale.step(d) / 2); - useAxis(groupElement); - - groupElement.select("path.domain").attr("visibility", "hidden"); - }); - - // exit - g.exit().attr("transform", (d, i) => translate(0, sign * getAxisOffset(i))); - }); - }; - - const scaleFromGroup = (scale, group) => { - function customScale(value) { - const values = value.domain; - return values.reduce((sum, d) => sum + scale(d), 0) / values.length; - } - - customScale.ticks = () => { - return group; - }; - customScale.tickFormat = () => d => { - return d.text; - }; - customScale.copy = () => scaleFromGroup(scale, group); - - customScale.step = value => value.domain.length * scale.step(); - - rebindAll(customScale, scale, exclude("ticks", "step", "copy")); - return customScale; - }; - - multiAxis.tickSize = (...args) => { - if (!args.length) { - return tickSizeInner; - } - tickSizeInner = tickSizeOuter = Number(args[0]); - return multiAxis; - }; - - multiAxis.tickSizeInner = (...args) => { - if (!args.length) { - return tickSizeInner; - } - tickSizeInner = Array.isArray(args[0]) ? args[0] : Number(args[0]); - return multiAxis; - }; - - multiAxis.tickSizeOuter = (...args) => { - if (!args.length) { - return tickSizeOuter; - } - tickSizeOuter = Number(args[0]); - return multiAxis; - }; - - multiAxis.decorate = (...args) => { - if (!args.length) { - return decorate; - } - decorate = args[0]; - return multiAxis; - }; - - multiAxis.groups = (...args) => { - if (!args.length) { - return groups; - } - groups = args[0]; - return multiAxis; - }; - - rebindAll(multiAxis, axisStore); - - return multiAxis; -}; - -export const multiAxisTop = scale => multiAxis("top", axisOrdinalTop, scale); - -export const multiAxisBottom = scale => multiAxis("bottom", axisOrdinalBottom, scale); - -export const multiAxisLeft = scale => multiAxis("left", axisOrdinalLeft, scale); - -export const multiAxisRight = scale => multiAxis("right", axisOrdinalRight, scale); +import {select, line} from "d3"; +import {axisOrdinalTop, axisOrdinalBottom, axisOrdinalLeft, axisOrdinalRight, dataJoin, rebindAll, exclude} from "d3fc"; +import store from "./store"; + +const multiAxis = (orient, baseAxis, scale) => { + let tickSizeOuter = 6; + let tickSizeInner = 6; + let axisStore = store("tickFormat", "ticks", "tickArguments", "tickValues", "tickPadding"); + let decorate = () => {}; + + let groups = null; + + const groupDataJoin = dataJoin("g", "group"); + const domainPathDataJoin = dataJoin("path", "domain"); + + const translate = (x, y) => (isVertical() ? `translate(${y}, ${x})` : `translate(${x}, ${y})`); + + const pathTranspose = arr => (isVertical() ? arr.map(d => [d[1], d[0]]) : arr); + + const isVertical = () => orient === "left" || orient === "right"; + + const multiAxis = selection => { + if (!groups) { + axisStore(baseAxis(scale).decorate(decorate))(selection); + return; + } + + if (selection.selection) { + groupDataJoin.transition(selection); + domainPathDataJoin.transition(selection); + } + + selection.each((data, index, group) => { + const element = group[index]; + + const container = select(element); + + const sign = orient === "bottom" || orient === "right" ? 1 : -1; + + // add the domain line + const range = scale.range(); + const domainPathData = pathTranspose([ + [range[0], sign * tickSizeOuter], + [range[0], 0], + [range[1], 0], + [range[1], sign * tickSizeOuter] + ]); + + const domainLine = domainPathDataJoin(container, [data]); + domainLine + .attr("d", line()(domainPathData)) + .attr("stroke", "#000") + .attr("fill", "none"); + + const g = groupDataJoin(container, groups); + + const getAxisSize = i => (Array.isArray(tickSizeInner) ? tickSizeInner[i] : tickSizeInner); + const getAxisOffset = i => { + let sum = 0; + for (let n = 0; n < i; n++) { + sum += getAxisSize(n); + } + return sum; + }; + + g.attr("transform", (d, i) => translate(0, sign * getAxisOffset(i))).each((group, i, nodes) => { + const groupElement = select(nodes[i]); + const groupScale = scaleFromGroup(scale, group); + const useAxis = axisStore(baseAxis(groupScale)) + .decorate((s, data) => decorate(s, data, i)) + .tickSizeInner(getAxisSize(i)) + .tickOffset(d => groupScale.step(d) / 2); + useAxis(groupElement); + + groupElement.select("path.domain").attr("visibility", "hidden"); + }); + + // exit + g.exit().attr("transform", (d, i) => translate(0, sign * getAxisOffset(i))); + }); + }; + + const scaleFromGroup = (scale, group) => { + function customScale(value) { + const values = value.domain; + return values.reduce((sum, d) => sum + scale(d), 0) / values.length; + } + + customScale.ticks = () => { + return group; + }; + customScale.tickFormat = () => d => { + return d.text; + }; + customScale.copy = () => scaleFromGroup(scale, group); + + customScale.step = value => value.domain.length * scale.step(); + + rebindAll(customScale, scale, exclude("ticks", "step", "copy")); + return customScale; + }; + + multiAxis.tickSize = (...args) => { + if (!args.length) { + return tickSizeInner; + } + tickSizeInner = tickSizeOuter = Number(args[0]); + return multiAxis; + }; + + multiAxis.tickSizeInner = (...args) => { + if (!args.length) { + return tickSizeInner; + } + tickSizeInner = Array.isArray(args[0]) ? args[0] : Number(args[0]); + return multiAxis; + }; + + multiAxis.tickSizeOuter = (...args) => { + if (!args.length) { + return tickSizeOuter; + } + tickSizeOuter = Number(args[0]); + return multiAxis; + }; + + multiAxis.decorate = (...args) => { + if (!args.length) { + return decorate; + } + decorate = args[0]; + return multiAxis; + }; + + multiAxis.groups = (...args) => { + if (!args.length) { + return groups; + } + groups = args[0]; + return multiAxis; + }; + + rebindAll(multiAxis, axisStore); + + return multiAxis; +}; + +export const multiAxisTop = scale => multiAxis("top", axisOrdinalTop, scale); + +export const multiAxisBottom = scale => multiAxis("bottom", axisOrdinalBottom, scale); + +export const multiAxisLeft = scale => multiAxis("left", axisOrdinalLeft, scale); + +export const multiAxisRight = scale => multiAxis("right", axisOrdinalRight, scale); diff --git a/packages/perspective-viewer-d3fc/src/js/d3fc/axis/store.js b/packages/perspective-viewer-d3fc/src/js/d3fc/axis/store.js index 3ed550c7d5..9e321b286c 100644 --- a/packages/perspective-viewer-d3fc/src/js/d3fc/axis/store.js +++ b/packages/perspective-viewer-d3fc/src/js/d3fc/axis/store.js @@ -1,22 +1,22 @@ -export default (...names) => { - const data = {}; - - const store = target => { - for (const key of Object.keys(data)) { - target[key](data[key]); - } - return target; - }; - - for (const name of names) { - store[name] = (...args) => { - if (!args.length) { - return data[name]; - } - data[name] = args[0]; - return store; - }; - } - - return store; -}; +export default (...names) => { + const data = {}; + + const store = target => { + for (const key of Object.keys(data)) { + target[key](data[key]); + } + return target; + }; + + for (const name of names) { + store[name] = (...args) => { + if (!args.length) { + return data[name]; + } + data[name] = args[0]; + return store; + }; + } + + return store; +}; diff --git a/packages/perspective-viewer-d3fc/src/js/data/findBest.js b/packages/perspective-viewer-d3fc/src/js/data/findBest.js index 85e0e98625..fc86965913 100644 --- a/packages/perspective-viewer-d3fc/src/js/data/findBest.js +++ b/packages/perspective-viewer-d3fc/src/js/data/findBest.js @@ -1,32 +1,32 @@ -/****************************************************************************** - * - * Copyright (c) 2017, the Perspective Authors. - * - * This file is part of the Perspective library, distributed under the terms of - * the Apache License 2.0. The full license can be found in the LICENSE file. - * - */ - -export const findBestFromData = (array, valueFn, compareFn = Math.min) => { - const findBestFromArray = array => { - return array.reduce((best, v) => { - const thisValue = findBestFromItem(v, valueFn); - return thisValue && (!best || compareFn(best.value, thisValue.value) === thisValue.value) ? thisValue : best; - }, null); - }; - const findBestFromItem = item => { - if (Array.isArray(item)) { - return findBestFromArray(item, valueFn); - } - const value = valueFn(item); - return value !== null - ? { - item, - value - } - : null; - }; - - const bestItem = findBestFromArray(array, valueFn); - return bestItem ? bestItem.item : null; -}; +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ + +export const findBestFromData = (array, valueFn, compareFn = Math.min) => { + const findBestFromArray = array => { + return array.reduce((best, v) => { + const thisValue = findBestFromItem(v, valueFn); + return thisValue && (!best || compareFn(best.value, thisValue.value) === thisValue.value) ? thisValue : best; + }, null); + }; + const findBestFromItem = item => { + if (Array.isArray(item)) { + return findBestFromArray(item, valueFn); + } + const value = valueFn(item); + return value !== null + ? { + item, + value + } + : null; + }; + + const bestItem = findBestFromArray(array, valueFn); + return bestItem ? bestItem.item : null; +}; diff --git a/packages/perspective-viewer-d3fc/src/js/data/pointData.js b/packages/perspective-viewer-d3fc/src/js/data/pointData.js index 7243b90e4c..7743a3c9b6 100644 --- a/packages/perspective-viewer-d3fc/src/js/data/pointData.js +++ b/packages/perspective-viewer-d3fc/src/js/data/pointData.js @@ -1,32 +1,32 @@ -/****************************************************************************** - * - * Copyright (c) 2017, the Perspective Authors. - * - * This file is part of the Perspective library, distributed under the terms of - * the Apache License 2.0. The full license can be found in the LICENSE file. - * - */ -import {labelFunction} from "../axis/axisLabel"; -import {splitIntoMultiSeries} from "./splitIntoMultiSeries"; - -export function pointData(settings, data) { - return splitIntoMultiSeries(settings, data, {excludeEmpty: true}).map(data => seriesToPoints(settings, data)); -} - -function seriesToPoints(settings, data) { - const labelfn = labelFunction(settings); - - const mappedSeries = data.map((col, i) => ({ - crossValue: labelfn(col, i), - mainValues: settings.mainValues.map(v => col[v.name]), - x: col[settings.mainValues[0].name], - y: col[settings.mainValues[1].name], - colorValue: settings.realValues[2] ? col[settings.realValues[2]] : undefined, - size: settings.realValues[3] ? col[settings.realValues[3]] : undefined, - key: data.key, - row: col - })); - - mappedSeries.key = data.key; - return mappedSeries; -} +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ +import {labelFunction} from "../axis/axisLabel"; +import {splitIntoMultiSeries} from "./splitIntoMultiSeries"; + +export function pointData(settings, data) { + return splitIntoMultiSeries(settings, data, {excludeEmpty: true}).map(data => seriesToPoints(settings, data)); +} + +function seriesToPoints(settings, data) { + const labelfn = labelFunction(settings); + + const mappedSeries = data.map((col, i) => ({ + crossValue: labelfn(col, i), + mainValues: settings.mainValues.map(v => col[v.name]), + x: col[settings.mainValues[0].name], + y: col[settings.mainValues[1].name], + colorValue: settings.realValues[2] ? col[settings.realValues[2]] : undefined, + size: settings.realValues[3] ? col[settings.realValues[3]] : undefined, + key: data.key, + row: col + })); + + mappedSeries.key = data.key; + return mappedSeries; +} diff --git a/packages/perspective-viewer-d3fc/src/js/data/splitData.js b/packages/perspective-viewer-d3fc/src/js/data/splitData.js index 1a44870c42..cbe073cfbf 100644 --- a/packages/perspective-viewer-d3fc/src/js/data/splitData.js +++ b/packages/perspective-viewer-d3fc/src/js/data/splitData.js @@ -1,24 +1,24 @@ -/****************************************************************************** - * - * Copyright (c) 2017, the Perspective Authors. - * - * This file is part of the Perspective library, distributed under the terms of - * the Apache License 2.0. The full license can be found in the LICENSE file. - * - */ -import {labelFunction} from "../axis/axisLabel"; - -export function splitData(settings, data) { - const labelfn = labelFunction(settings); - - return data.map((col, i) => { - return Object.keys(col) - .filter(key => key !== "__ROW_PATH__") - .map(key => ({ - key, - crossValue: labelfn(col, i), - mainValue: col[key], - row: col - })); - }); -} +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ +import {labelFunction} from "../axis/axisLabel"; + +export function splitData(settings, data) { + const labelfn = labelFunction(settings); + + return data.map((col, i) => { + return Object.keys(col) + .filter(key => key !== "__ROW_PATH__") + .map(key => ({ + key, + crossValue: labelfn(col, i), + mainValue: col[key], + row: col + })); + }); +} diff --git a/packages/perspective-viewer-d3fc/src/js/data/splitIntoMultiSeries.js b/packages/perspective-viewer-d3fc/src/js/data/splitIntoMultiSeries.js index f8a021351e..ac46a4fd53 100644 --- a/packages/perspective-viewer-d3fc/src/js/data/splitIntoMultiSeries.js +++ b/packages/perspective-viewer-d3fc/src/js/data/splitIntoMultiSeries.js @@ -1,73 +1,73 @@ -/****************************************************************************** - * - * Copyright (c) 2017, the Perspective Authors. - * - * This file is part of the Perspective library, distributed under the terms of - * the Apache License 2.0. The full license can be found in the LICENSE file. - * - */ - -export function splitIntoMultiSeries(settings, data, {stack = false, excludeEmpty = false} = {}) { - const useData = data || settings.data; - - if (settings.splitValues.length > 0) { - return splitByValuesIntoMultiSeries(settings, useData, {stack, excludeEmpty}); - } - return [useData]; -} - -function splitByValuesIntoMultiSeries(settings, data, {stack = false, excludeEmpty = false}) { - // Create a series for each "split" value, each one containing all the - // "aggregate" values, and "base" values to offset it from the previous - // series - const multiSeries = {}; - - data.forEach(col => { - // Split this column by "split", including multiple aggregates for each - const baseValues = {}; - const split = {}; - - // Keys are of the form "split1|split2|aggregate" - Object.keys(col) - .filter(key => key !== "__ROW_PATH__") - .filter(key => !excludeEmpty || (col[key] != null && col[key] != undefined)) - .forEach(key => { - const labels = key.split("|"); - // label="aggregate" - const label = labels[labels.length - 1]; - const value = col[key] || 0; - const baseKey = `${label}${value >= 0 ? "+ve" : "-ve"}`; - // splitName="split1|split2" - const splitName = labels.slice(0, labels.length - 1).join("|"); - - // Combine aggregate values for the same split in a single - // object - const splitValues = (split[splitName] = split[splitName] || {__ROW_PATH__: col.__ROW_PATH__}); - const baseValue = baseValues[baseKey] || 0; - - splitValues.__KEY__ = splitName; - - // Assign the values for this split/aggregate - if (stack) { - splitValues[label] = baseValue + value; - splitValues[`__BASE_VALUE__${label}`] = baseValue; - baseValues[baseKey] = splitValues[label]; - } else { - splitValues[label] = value; - } - splitValues.row = col; - }); - - // Push each object onto the correct series - Object.keys(split).forEach(splitName => { - const series = (multiSeries[splitName] = multiSeries[splitName] || []); - series.push(split[splitName]); - }); - }); - - return Object.keys(multiSeries).map(k => { - const series = multiSeries[k]; - series.key = k; - return series; - }); -} +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ + +export function splitIntoMultiSeries(settings, data, {stack = false, excludeEmpty = false} = {}) { + const useData = data || settings.data; + + if (settings.splitValues.length > 0) { + return splitByValuesIntoMultiSeries(settings, useData, {stack, excludeEmpty}); + } + return [useData]; +} + +function splitByValuesIntoMultiSeries(settings, data, {stack = false, excludeEmpty = false}) { + // Create a series for each "split" value, each one containing all the + // "aggregate" values, and "base" values to offset it from the previous + // series + const multiSeries = {}; + + data.forEach(col => { + // Split this column by "split", including multiple aggregates for each + const baseValues = {}; + const split = {}; + + // Keys are of the form "split1|split2|aggregate" + Object.keys(col) + .filter(key => key !== "__ROW_PATH__") + .filter(key => !excludeEmpty || (col[key] != null && col[key] != undefined)) + .forEach(key => { + const labels = key.split("|"); + // label="aggregate" + const label = labels[labels.length - 1]; + const value = col[key] || 0; + const baseKey = `${label}${value >= 0 ? "+ve" : "-ve"}`; + // splitName="split1|split2" + const splitName = labels.slice(0, labels.length - 1).join("|"); + + // Combine aggregate values for the same split in a single + // object + const splitValues = (split[splitName] = split[splitName] || {__ROW_PATH__: col.__ROW_PATH__}); + const baseValue = baseValues[baseKey] || 0; + + splitValues.__KEY__ = splitName; + + // Assign the values for this split/aggregate + if (stack) { + splitValues[label] = baseValue + value; + splitValues[`__BASE_VALUE__${label}`] = baseValue; + baseValues[baseKey] = splitValues[label]; + } else { + splitValues[label] = value; + } + splitValues.row = col; + }); + + // Push each object onto the correct series + Object.keys(split).forEach(splitName => { + const series = (multiSeries[splitName] = multiSeries[splitName] || []); + series.push(split[splitName]); + }); + }); + + return Object.keys(multiSeries).map(k => { + const series = multiSeries[k]; + series.key = k; + return series; + }); +} diff --git a/packages/perspective-viewer-d3fc/src/js/legend/filter.js b/packages/perspective-viewer-d3fc/src/js/legend/filter.js index d712baf4f9..508597729a 100644 --- a/packages/perspective-viewer-d3fc/src/js/legend/filter.js +++ b/packages/perspective-viewer-d3fc/src/js/legend/filter.js @@ -1,39 +1,39 @@ -/****************************************************************************** - * - * Copyright (c) 2017, the Perspective Authors. - * - * This file is part of the Perspective library, distributed under the terms of - * the Apache License 2.0. The full license can be found in the LICENSE file. - * - */ -import {groupFromKey} from "../series/seriesKey"; - -export function filterData(settings, data) { - const useData = data || settings.data; - if (settings.hideKeys && settings.hideKeys.length > 0) { - return useData.map(col => { - const clone = {...col}; - settings.hideKeys.forEach(k => { - delete clone[k]; - }); - return clone; - }); - } - return useData; -} - -export function filterDataByGroup(settings, data) { - const useData = data || settings.data; - if (settings.hideKeys && settings.hideKeys.length > 0) { - return useData.map(col => { - const clone = {}; - Object.keys(col).map(key => { - if (!settings.hideKeys.includes(groupFromKey(key))) { - clone[key] = col[key]; - } - }); - return clone; - }); - } - return useData; -} +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ +import {groupFromKey} from "../series/seriesKey"; + +export function filterData(settings, data) { + const useData = data || settings.data; + if (settings.hideKeys && settings.hideKeys.length > 0) { + return useData.map(col => { + const clone = {...col}; + settings.hideKeys.forEach(k => { + delete clone[k]; + }); + return clone; + }); + } + return useData; +} + +export function filterDataByGroup(settings, data) { + const useData = data || settings.data; + if (settings.hideKeys && settings.hideKeys.length > 0) { + return useData.map(col => { + const clone = {}; + Object.keys(col).map(key => { + if (!settings.hideKeys.includes(groupFromKey(key))) { + clone[key] = col[key]; + } + }); + return clone; + }); + } + return useData; +} diff --git a/packages/perspective-viewer-d3fc/src/js/legend/scrollableLegend.js b/packages/perspective-viewer-d3fc/src/js/legend/scrollableLegend.js index 7323ea20d9..a51eb2375a 100644 --- a/packages/perspective-viewer-d3fc/src/js/legend/scrollableLegend.js +++ b/packages/perspective-viewer-d3fc/src/js/legend/scrollableLegend.js @@ -1,135 +1,135 @@ -/****************************************************************************** - * - * Copyright (c) 2017, the Perspective Authors. - * - * This file is part of the Perspective library, distributed under the terms of - * the Apache License 2.0. The full license can be found in the LICENSE file. - * - */ -import * as d3Legend from "d3-svg-legend"; -import {rebindAll} from "d3fc"; -import {getOrCreateElement} from "../utils/utils"; -import legendControlsTemplate from "../../html/legend-controls.html"; -import {cropCellContents} from "./styling/cropCellContents"; -import {draggableComponent} from "./styling/draggableComponent"; -import {resizableComponent} from "./styling/resizableComponent"; - -const averageCellHeightPx = 16; -const controlsHeightPx = 20; - -export default (fromLegend, settings) => { - const legend = fromLegend || d3Legend.legendColor(); - - let domain = []; - let pageCount = 1; - let pageSize; - let pageIndex = settings.legend && settings.legend.pageIndex ? settings.legend.pageIndex : 0; - let decorate = () => {}; - let draggable = draggableComponent().settings(settings); - let resizable; - - const scrollableLegend = selection => { - domain = legend.scale().domain(); - - resizable = resizableComponent() - .settings(settings) - .maxHeight(domain.length * averageCellHeightPx + controlsHeightPx) - .on("resize", () => render(selection)); - - resizable(selection); - draggable(selection); - render(selection); - }; - - const render = selection => { - calculatePageSize(selection); - renderControls(selection); - renderLegend(selection); - cropCellContents(selection); - }; - - const renderControls = selection => { - const controls = getLegendControls(selection); - controls.style("display", pageCount <= 1 ? "none" : "block"); - - controls.select("#page-text").text(`${pageIndex + 1}/${pageCount}`); - - controls - .select("#up-arrow") - .attr("class", pageIndex === 0 ? "disabled" : "") - .on("click", () => { - if (pageIndex > 0) { - setPage(pageIndex - 1); - render(selection); - } - }); - - controls - .select("#down-arrow") - .attr("class", pageIndex >= pageCount - 1 ? "disabled" : "") - .on("click", () => { - if (pageIndex < pageCount - 1) { - setPage(pageIndex + 1); - render(selection); - } - }); - }; - - const renderLegend = selection => { - if (pageCount > 1) legend.cellFilter(cellFilter()); - selection.select("g.legendCells").remove(); - - const legendElement = getLegendElement(selection); - legendElement.call(legend); - - const cellContainerSize = selection - .select("g.legendCells") - .node() - .getBBox(); - legendElement.attr("height", cellContainerSize.height + controlsHeightPx); - - decorate(selection); - }; - - const setPage = index => { - pageIndex = index; - settings.legend = {...settings.legend, pageIndex}; - }; - - const cellFilter = () => (_, i) => i >= pageSize * pageIndex && i < pageSize * pageIndex + pageSize; - - const calculatePageSize = selection => { - const legendContainerRect = selection.node().getBoundingClientRect(); - let proposedPageSize = Math.floor(legendContainerRect.height / averageCellHeightPx) - 1; - - // if page size is less than all legend items, leave space for the - // legend controls - pageSize = proposedPageSize < domain.length ? proposedPageSize - 1 : proposedPageSize; - pageCount = calculatePageCount(proposedPageSize); - pageIndex = Math.min(pageIndex, pageCount - 1); - }; - - const calculatePageCount = pageSize => Math.ceil(domain.length / pageSize); - - const getLegendControls = container => - getOrCreateElement(container, ".legend-controls", () => - container - .append("g") - .attr("class", "legend-controls") - .html(legendControlsTemplate) - ); - - const getLegendElement = container => getOrCreateElement(container, ".legend", () => container.append("svg").attr("class", "legend")); - - scrollableLegend.decorate = (...args) => { - if (!args.length) { - return decorate; - } - decorate = args[0]; - return scrollableLegend; - }; - - rebindAll(scrollableLegend, legend); - - return scrollableLegend; -}; +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ +import * as d3Legend from "d3-svg-legend"; +import {rebindAll} from "d3fc"; +import {getOrCreateElement} from "../utils/utils"; +import legendControlsTemplate from "../../html/legend-controls.html"; +import {cropCellContents} from "./styling/cropCellContents"; +import {draggableComponent} from "./styling/draggableComponent"; +import {resizableComponent} from "./styling/resizableComponent"; + +const averageCellHeightPx = 16; +const controlsHeightPx = 20; + +export default (fromLegend, settings) => { + const legend = fromLegend || d3Legend.legendColor(); + + let domain = []; + let pageCount = 1; + let pageSize; + let pageIndex = settings.legend && settings.legend.pageIndex ? settings.legend.pageIndex : 0; + let decorate = () => {}; + let draggable = draggableComponent().settings(settings); + let resizable; + + const scrollableLegend = selection => { + domain = legend.scale().domain(); + + resizable = resizableComponent() + .settings(settings) + .maxHeight(domain.length * averageCellHeightPx + controlsHeightPx) + .on("resize", () => render(selection)); + + resizable(selection); + draggable(selection); + render(selection); + }; + + const render = selection => { + calculatePageSize(selection); + renderControls(selection); + renderLegend(selection); + cropCellContents(selection); + }; + + const renderControls = selection => { + const controls = getLegendControls(selection); + controls.style("display", pageCount <= 1 ? "none" : "block"); + + controls.select("#page-text").text(`${pageIndex + 1}/${pageCount}`); + + controls + .select("#up-arrow") + .attr("class", pageIndex === 0 ? "disabled" : "") + .on("click", () => { + if (pageIndex > 0) { + setPage(pageIndex - 1); + render(selection); + } + }); + + controls + .select("#down-arrow") + .attr("class", pageIndex >= pageCount - 1 ? "disabled" : "") + .on("click", () => { + if (pageIndex < pageCount - 1) { + setPage(pageIndex + 1); + render(selection); + } + }); + }; + + const renderLegend = selection => { + if (pageCount > 1) legend.cellFilter(cellFilter()); + selection.select("g.legendCells").remove(); + + const legendElement = getLegendElement(selection); + legendElement.call(legend); + + const cellContainerSize = selection + .select("g.legendCells") + .node() + .getBBox(); + legendElement.attr("height", cellContainerSize.height + controlsHeightPx); + + decorate(selection); + }; + + const setPage = index => { + pageIndex = index; + settings.legend = {...settings.legend, pageIndex}; + }; + + const cellFilter = () => (_, i) => i >= pageSize * pageIndex && i < pageSize * pageIndex + pageSize; + + const calculatePageSize = selection => { + const legendContainerRect = selection.node().getBoundingClientRect(); + let proposedPageSize = Math.floor(legendContainerRect.height / averageCellHeightPx) - 1; + + // if page size is less than all legend items, leave space for the + // legend controls + pageSize = proposedPageSize < domain.length ? proposedPageSize - 1 : proposedPageSize; + pageCount = calculatePageCount(proposedPageSize); + pageIndex = Math.min(pageIndex, pageCount - 1); + }; + + const calculatePageCount = pageSize => Math.ceil(domain.length / pageSize); + + const getLegendControls = container => + getOrCreateElement(container, ".legend-controls", () => + container + .append("g") + .attr("class", "legend-controls") + .html(legendControlsTemplate) + ); + + const getLegendElement = container => getOrCreateElement(container, ".legend", () => container.append("svg").attr("class", "legend")); + + scrollableLegend.decorate = (...args) => { + if (!args.length) { + return decorate; + } + decorate = args[0]; + return scrollableLegend; + }; + + rebindAll(scrollableLegend, legend); + + return scrollableLegend; +}; diff --git a/packages/perspective-viewer-d3fc/src/js/plugin/plugin.js b/packages/perspective-viewer-d3fc/src/js/plugin/plugin.js index 199dd5658a..0369ed0520 100644 --- a/packages/perspective-viewer-d3fc/src/js/plugin/plugin.js +++ b/packages/perspective-viewer-d3fc/src/js/plugin/plugin.js @@ -1,131 +1,131 @@ -/****************************************************************************** - * - * Copyright (c) 2017, the Perspective Authors. - * - * This file is part of the Perspective library, distributed under the terms of - * the Apache License 2.0. The full license can be found in the LICENSE file. - * - */ - -import {registerPlugin} from "@finos/perspective-viewer/dist/esm/utils.js"; -import charts from "../charts/charts"; -import "./polyfills/index"; -import "./template"; - -export const PRIVATE = Symbol("D3FC chart"); - -const DEFAULT_PLUGIN_SETTINGS = { - initial: { - type: "number", - count: 1 - }, - selectMode: "select" -}; - -export function register(...plugins) { - plugins = new Set(plugins.length > 0 ? plugins : charts.map(chart => chart.plugin.type)); - charts.forEach(chart => { - if (plugins.has(chart.plugin.type)) { - registerPlugin(chart.plugin.type, { - ...DEFAULT_PLUGIN_SETTINGS, - ...chart.plugin, - create: drawChart(chart), - resize: resizeChart, - delete: deleteChart, - save, - restore - }); - } - }); -} - -function drawChart(chart) { - return async function(el, view, task, end_col, end_row) { - let jsonp; - const realValues = JSON.parse(this.getAttribute("columns")); - - if (end_col && end_row) { - jsonp = view.to_json({end_row, end_col, leaves_only: true}); - } else if (end_col) { - jsonp = view.to_json({end_col, leaves_only: true}); - } else if (end_row) { - jsonp = view.to_json({end_row, leaves_only: true}); - } else { - jsonp = view.to_json({leaves_only: true}); - } - - let [tschema, schema, json, config] = await Promise.all([this._table.schema(false), view.schema(false), jsonp, view.get_config()]); - - if (task.cancelled) { - return; - } - - const {columns, row_pivots, column_pivots, filter} = config; - const filtered = row_pivots.length > 0 ? json.filter(col => col.__ROW_PATH__ && col.__ROW_PATH__.length == row_pivots.length) : json; - const dataMap = (col, i) => (!row_pivots.length ? {...col, __ROW_PATH__: [i]} : col); - const mapped = filtered.map(dataMap); - - let settings = { - realValues, - crossValues: row_pivots.map(r => ({name: r, type: tschema[r]})), - mainValues: columns.map(a => ({name: a, type: schema[a]})), - splitValues: column_pivots.map(r => ({name: r, type: tschema[r]})), - filter, - data: mapped - }; - - createOrUpdateChart.call(this, el, chart, settings); - }; -} - -function getOrCreatePluginElement() { - let perspective_d3fc_element; - this[PRIVATE] = this[PRIVATE] || {}; - if (!this[PRIVATE].chart) { - perspective_d3fc_element = this[PRIVATE].chart = document.createElement("perspective-d3fc-chart"); - } else { - perspective_d3fc_element = this[PRIVATE].chart; - } - return perspective_d3fc_element; -} - -function createOrUpdateChart(div, chart, settings) { - const perspective_d3fc_element = getOrCreatePluginElement.call(this); - - if (!document.body.contains(perspective_d3fc_element)) { - div.innerHTML = ""; - div.appendChild(perspective_d3fc_element); - } - - perspective_d3fc_element.render(chart, settings); -} - -function resizeChart() { - if (this[PRIVATE] && this[PRIVATE].chart) { - const perspective_d3fc_element = this[PRIVATE].chart; - perspective_d3fc_element.resize(); - } -} - -function deleteChart() { - if (this[PRIVATE] && this[PRIVATE].chart) { - const perspective_d3fc_element = this[PRIVATE].chart; - perspective_d3fc_element.delete(); - } -} - -function save() { - if (this[PRIVATE] && this[PRIVATE].chart) { - const perspective_d3fc_element = this[PRIVATE].chart; - return perspective_d3fc_element.getSettings(); - } -} - -function restore(config) { - const perspective_d3fc_element = getOrCreatePluginElement.call(this); - perspective_d3fc_element.setSettings(config); -} - -if (!Element.prototype.matches) { - Element.prototype.matches = Element.prototype.msMatchesSelector; -} +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ + +import {registerPlugin} from "@finos/perspective-viewer/dist/esm/utils.js"; +import charts from "../charts/charts"; +import "./polyfills/index"; +import "./template"; + +export const PRIVATE = Symbol("D3FC chart"); + +const DEFAULT_PLUGIN_SETTINGS = { + initial: { + type: "number", + count: 1 + }, + selectMode: "select" +}; + +export function register(...plugins) { + plugins = new Set(plugins.length > 0 ? plugins : charts.map(chart => chart.plugin.type)); + charts.forEach(chart => { + if (plugins.has(chart.plugin.type)) { + registerPlugin(chart.plugin.type, { + ...DEFAULT_PLUGIN_SETTINGS, + ...chart.plugin, + create: drawChart(chart), + resize: resizeChart, + delete: deleteChart, + save, + restore + }); + } + }); +} + +function drawChart(chart) { + return async function(el, view, task, end_col, end_row) { + let jsonp; + const realValues = JSON.parse(this.getAttribute("columns")); + + if (end_col && end_row) { + jsonp = view.to_json({end_row, end_col, leaves_only: true}); + } else if (end_col) { + jsonp = view.to_json({end_col, leaves_only: true}); + } else if (end_row) { + jsonp = view.to_json({end_row, leaves_only: true}); + } else { + jsonp = view.to_json({leaves_only: true}); + } + + let [tschema, schema, json, config] = await Promise.all([this._table.schema(false), view.schema(false), jsonp, view.get_config()]); + + if (task.cancelled) { + return; + } + + const {columns, row_pivots, column_pivots, filter} = config; + const filtered = row_pivots.length > 0 ? json.filter(col => col.__ROW_PATH__ && col.__ROW_PATH__.length == row_pivots.length) : json; + const dataMap = (col, i) => (!row_pivots.length ? {...col, __ROW_PATH__: [i]} : col); + const mapped = filtered.map(dataMap); + + let settings = { + realValues, + crossValues: row_pivots.map(r => ({name: r, type: tschema[r]})), + mainValues: columns.map(a => ({name: a, type: schema[a]})), + splitValues: column_pivots.map(r => ({name: r, type: tschema[r]})), + filter, + data: mapped + }; + + createOrUpdateChart.call(this, el, chart, settings); + }; +} + +function getOrCreatePluginElement() { + let perspective_d3fc_element; + this[PRIVATE] = this[PRIVATE] || {}; + if (!this[PRIVATE].chart) { + perspective_d3fc_element = this[PRIVATE].chart = document.createElement("perspective-d3fc-chart"); + } else { + perspective_d3fc_element = this[PRIVATE].chart; + } + return perspective_d3fc_element; +} + +function createOrUpdateChart(div, chart, settings) { + const perspective_d3fc_element = getOrCreatePluginElement.call(this); + + if (!document.body.contains(perspective_d3fc_element)) { + div.innerHTML = ""; + div.appendChild(perspective_d3fc_element); + } + + perspective_d3fc_element.render(chart, settings); +} + +function resizeChart() { + if (this[PRIVATE] && this[PRIVATE].chart) { + const perspective_d3fc_element = this[PRIVATE].chart; + perspective_d3fc_element.resize(); + } +} + +function deleteChart() { + if (this[PRIVATE] && this[PRIVATE].chart) { + const perspective_d3fc_element = this[PRIVATE].chart; + perspective_d3fc_element.delete(); + } +} + +function save() { + if (this[PRIVATE] && this[PRIVATE].chart) { + const perspective_d3fc_element = this[PRIVATE].chart; + return perspective_d3fc_element.getSettings(); + } +} + +function restore(config) { + const perspective_d3fc_element = getOrCreatePluginElement.call(this); + perspective_d3fc_element.setSettings(config); +} + +if (!Element.prototype.matches) { + Element.prototype.matches = Element.prototype.msMatchesSelector; +} diff --git a/packages/perspective-viewer-d3fc/src/js/plugin/polyfills/closest.js b/packages/perspective-viewer-d3fc/src/js/plugin/polyfills/closest.js index bf2da8264c..154a21c7d7 100644 --- a/packages/perspective-viewer-d3fc/src/js/plugin/polyfills/closest.js +++ b/packages/perspective-viewer-d3fc/src/js/plugin/polyfills/closest.js @@ -1,23 +1,23 @@ -/****************************************************************************** - * - * Copyright (c) 2017, the Perspective Authors. - * - * This file is part of the Perspective library, distributed under the terms of - * the Apache License 2.0. The full license can be found in the LICENSE file. - * - */ -if (!Element.prototype.matches) { - Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector; -} - -if (!Element.prototype.closest) { - Element.prototype.closest = function(s) { - var el = this; - - do { - if (el.matches(s)) return el; - el = el.parentElement || el.parentNode; - } while (el !== null && el.nodeType === 1); - return null; - }; -} +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ +if (!Element.prototype.matches) { + Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector; +} + +if (!Element.prototype.closest) { + Element.prototype.closest = function(s) { + var el = this; + + do { + if (el.matches(s)) return el; + el = el.parentElement || el.parentNode; + } while (el !== null && el.nodeType === 1); + return null; + }; +} diff --git a/packages/perspective-viewer-d3fc/src/js/plugin/polyfills/index.js b/packages/perspective-viewer-d3fc/src/js/plugin/polyfills/index.js index 8cc9536b52..da9ed2da49 100644 --- a/packages/perspective-viewer-d3fc/src/js/plugin/polyfills/index.js +++ b/packages/perspective-viewer-d3fc/src/js/plugin/polyfills/index.js @@ -1,2 +1,2 @@ -import "./matches"; -import "./closest"; +import "./matches"; +import "./closest"; diff --git a/packages/perspective-viewer-d3fc/src/js/plugin/polyfills/matches.js b/packages/perspective-viewer-d3fc/src/js/plugin/polyfills/matches.js index 96e76ea0b1..a3865853c4 100644 --- a/packages/perspective-viewer-d3fc/src/js/plugin/polyfills/matches.js +++ b/packages/perspective-viewer-d3fc/src/js/plugin/polyfills/matches.js @@ -1,11 +1,11 @@ -/****************************************************************************** - * - * Copyright (c) 2017, the Perspective Authors. - * - * This file is part of the Perspective library, distributed under the terms of - * the Apache License 2.0. The full license can be found in the LICENSE file. - * - */ -if (!Element.prototype.matches) { - Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector; -} +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ +if (!Element.prototype.matches) { + Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector; +} diff --git a/packages/perspective-viewer-d3fc/src/js/plugin/root.js b/packages/perspective-viewer-d3fc/src/js/plugin/root.js index 24cfe7e541..932f8eae8c 100644 --- a/packages/perspective-viewer-d3fc/src/js/plugin/root.js +++ b/packages/perspective-viewer-d3fc/src/js/plugin/root.js @@ -1,16 +1,16 @@ -/****************************************************************************** - * - * Copyright (c) 2017, the Perspective Authors. - * - * This file is part of the Perspective library, distributed under the terms of - * the Apache License 2.0. The full license can be found in the LICENSE file. - * - */ - -export function getChartElement(element) { - return element.getRootNode().host; -} - -export function getChartContainer(element) { - return element.closest("#container.chart"); -} +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ + +export function getChartElement(element) { + return element.getRootNode().host; +} + +export function getChartContainer(element) { + return element.closest("#container.chart"); +} diff --git a/packages/perspective-viewer-d3fc/src/js/plugin/template.js b/packages/perspective-viewer-d3fc/src/js/plugin/template.js index ab525dd1c1..f6f08b81d6 100644 --- a/packages/perspective-viewer-d3fc/src/js/plugin/template.js +++ b/packages/perspective-viewer-d3fc/src/js/plugin/template.js @@ -1,119 +1,119 @@ -/****************************************************************************** - * - * Copyright (c) 2017, the Perspective Authors. - * - * This file is part of the Perspective library, distributed under the terms of - * the Apache License 2.0. The full license can be found in the LICENSE file. - * - */ -import * as d3 from "d3"; - -import style from "../../less/chart.less"; -import template from "../../html/d3fc-chart.html"; -import {areArraysEqualSimple} from "../utils/utils"; -import {initialiseStyles} from "../series/colorStyles"; - -import {bindTemplate} from "@finos/perspective-viewer/dist/esm/utils"; - -const styleWithD3FC = `${style}${getD3FCStyles()}`; - -@bindTemplate(template, styleWithD3FC) // eslint-disable-next-line no-unused-vars -class D3FCChartElement extends HTMLElement { - constructor() { - super(); - this._chart = null; - this._settings = null; - } - - connectedCallback() { - console.log("connected callback"); - this._container = this.shadowRoot.querySelector(".chart"); - } - - render(chart, settings) { - this._chart = chart; - this._settings = this._configureSettings(this._settings, settings); - initialiseStyles(this._container, this._settings); - - if ((this._settings.data && this._settings.data.length > 0) || chart.plugin.type !== this._chart.plugin.type) { - this.remove(); - } - this.draw(); - - if (window.navigator.userAgent.indexOf("Edge") > -1) { - // Workaround for MS Edge issue that doesn't draw content in the - // plot-area until the chart D3 object is redrawn - setTimeout(() => this.draw()); - } - } - - draw() { - if (this._settings.data) { - const containerDiv = d3.select(this._container); - const chartClass = `chart ${this._chart.plugin.type}`; - this._settings.size = this._container.getBoundingClientRect(); - - if (this._settings.data.length > 0) { - this._chart(containerDiv.attr("class", chartClass), this._settings); - } else { - containerDiv.attr("class", `${chartClass} disabled`); - } - } - } - - resize() { - this.draw(); - } - - remove() { - this._container.innerHTML = ""; - } - - delete() { - this.remove(); - } - - getContainer() { - return this._container; - } - - getSettings() { - const excludeSettings = ["crossValues", "mainValues", "splitValues", "filter", "data", "size", "colorStyles"]; - const settings = {...this._settings}; - excludeSettings.forEach(s => { - delete settings[s]; - }); - return settings; - } - - setSettings(settings) { - this._settings = {...this._settings, ...settings}; - this.draw(); - } - - _configureSettings(oldSettings, newSettings) { - if (oldSettings) { - if (!oldSettings.data) { - // Combine with the restored settings - return {...oldSettings, ...newSettings}; - } - - const oldValues = [oldSettings.crossValues, oldSettings.mainValues, oldSettings.splitValues, oldSettings.realValues]; - const newValues = [newSettings.crossValues, newSettings.mainValues, newSettings.splitValues, newSettings.realValues]; - if (areArraysEqualSimple(oldValues, newValues)) return {...oldSettings, data: newSettings.data, colorStyles: null}; - } - this.remove(); - return newSettings; - } -} - -function getD3FCStyles() { - const headerStyles = document.querySelector("head").querySelectorAll("style"); - const d3fcStyles = []; - headerStyles.forEach(s => { - if (s.innerText.indexOf("d3fc-") !== -1) { - d3fcStyles.push(s.innerText); - } - }); - return d3fcStyles.join(""); -} +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ +import * as d3 from "d3"; + +import style from "../../less/chart.less"; +import template from "../../html/d3fc-chart.html"; +import {areArraysEqualSimple} from "../utils/utils"; +import {initialiseStyles} from "../series/colorStyles"; + +import {bindTemplate} from "@finos/perspective-viewer/dist/esm/utils"; + +const styleWithD3FC = `${style}${getD3FCStyles()}`; + +@bindTemplate(template, styleWithD3FC) // eslint-disable-next-line no-unused-vars +class D3FCChartElement extends HTMLElement { + constructor() { + super(); + this._chart = null; + this._settings = null; + } + + connectedCallback() { + console.log("connected callback"); + this._container = this.shadowRoot.querySelector(".chart"); + } + + render(chart, settings) { + this._chart = chart; + this._settings = this._configureSettings(this._settings, settings); + initialiseStyles(this._container, this._settings); + + if ((this._settings.data && this._settings.data.length > 0) || chart.plugin.type !== this._chart.plugin.type) { + this.remove(); + } + this.draw(); + + if (window.navigator.userAgent.indexOf("Edge") > -1) { + // Workaround for MS Edge issue that doesn't draw content in the + // plot-area until the chart D3 object is redrawn + setTimeout(() => this.draw()); + } + } + + draw() { + if (this._settings.data) { + const containerDiv = d3.select(this._container); + const chartClass = `chart ${this._chart.plugin.type}`; + this._settings.size = this._container.getBoundingClientRect(); + + if (this._settings.data.length > 0) { + this._chart(containerDiv.attr("class", chartClass), this._settings); + } else { + containerDiv.attr("class", `${chartClass} disabled`); + } + } + } + + resize() { + this.draw(); + } + + remove() { + this._container.innerHTML = ""; + } + + delete() { + this.remove(); + } + + getContainer() { + return this._container; + } + + getSettings() { + const excludeSettings = ["crossValues", "mainValues", "splitValues", "filter", "data", "size", "colorStyles"]; + const settings = {...this._settings}; + excludeSettings.forEach(s => { + delete settings[s]; + }); + return settings; + } + + setSettings(settings) { + this._settings = {...this._settings, ...settings}; + this.draw(); + } + + _configureSettings(oldSettings, newSettings) { + if (oldSettings) { + if (!oldSettings.data) { + // Combine with the restored settings + return {...oldSettings, ...newSettings}; + } + + const oldValues = [oldSettings.crossValues, oldSettings.mainValues, oldSettings.splitValues, oldSettings.realValues]; + const newValues = [newSettings.crossValues, newSettings.mainValues, newSettings.splitValues, newSettings.realValues]; + if (areArraysEqualSimple(oldValues, newValues)) return {...oldSettings, data: newSettings.data, colorStyles: null}; + } + this.remove(); + return newSettings; + } +} + +function getD3FCStyles() { + const headerStyles = document.querySelector("head").querySelectorAll("style"); + const d3fcStyles = []; + headerStyles.forEach(s => { + if (s.innerText.indexOf("d3fc-") !== -1) { + d3fcStyles.push(s.innerText); + } + }); + return d3fcStyles.join(""); +} diff --git a/packages/perspective-viewer-d3fc/src/js/series/areaSeries.js b/packages/perspective-viewer-d3fc/src/js/series/areaSeries.js index 69f3ff99b5..4eb24d47c2 100644 --- a/packages/perspective-viewer-d3fc/src/js/series/areaSeries.js +++ b/packages/perspective-viewer-d3fc/src/js/series/areaSeries.js @@ -1,22 +1,22 @@ -/****************************************************************************** - * - * Copyright (c) 2017, the Perspective Authors. - * - * This file is part of the Perspective library, distributed under the terms of - * the Apache License 2.0. The full license can be found in the LICENSE file. - * - */ -import * as fc from "d3fc"; - -export function areaSeries(settings, color) { - let series = fc.seriesSvgArea(); - - series = series.decorate(selection => { - selection.style("fill", d => color(d[0].key)); - }); - - return series - .crossValue(d => d.crossValue) - .mainValue(d => d.mainValue) - .baseValue(d => d.baseValue); -} +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ +import * as fc from "d3fc"; + +export function areaSeries(settings, color) { + let series = fc.seriesSvgArea(); + + series = series.decorate(selection => { + selection.style("fill", d => color(d[0].key)); + }); + + return series + .crossValue(d => d.crossValue) + .mainValue(d => d.mainValue) + .baseValue(d => d.baseValue); +} diff --git a/packages/perspective-viewer-d3fc/src/js/series/barSeries.js b/packages/perspective-viewer-d3fc/src/js/series/barSeries.js index 261b9c8e1a..b48f411052 100644 --- a/packages/perspective-viewer-d3fc/src/js/series/barSeries.js +++ b/packages/perspective-viewer-d3fc/src/js/series/barSeries.js @@ -1,43 +1,43 @@ -/****************************************************************************** - * - * Copyright (c) 2017, the Perspective Authors. - * - * This file is part of the Perspective library, distributed under the terms of - * the Apache License 2.0. The full license can be found in the LICENSE file. - * - */ -import * as fc from "d3fc"; -import {tooltip} from "../tooltip/tooltip"; - -export function barSeries(settings, color) { - let series = settings.mainValues.length > 1 ? fc.seriesSvgGrouped(fc.seriesSvgBar()) : fc.seriesSvgBar(); - - series = series.decorate(selection => { - tooltip().settings(settings)(selection); - selection.style("fill", d => color(d.key)); - }); - - return fc - .autoBandwidth(minBandwidth(series)) - .crossValue(d => d.crossValue) - .mainValue(d => d.mainValue) - .baseValue(d => d.baseValue); -} - -const minBandwidth = adaptee => { - const min = arg => { - return adaptee(arg); - }; - - fc.rebindAll(min, adaptee); - - min.bandwidth = (...args) => { - if (!args.length) { - return adaptee.bandwidth(); - } - adaptee.bandwidth(Math.max(args[0], 1)); - return min; - }; - - return min; -}; +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ +import * as fc from "d3fc"; +import {tooltip} from "../tooltip/tooltip"; + +export function barSeries(settings, color) { + let series = settings.mainValues.length > 1 ? fc.seriesSvgGrouped(fc.seriesSvgBar()) : fc.seriesSvgBar(); + + series = series.decorate(selection => { + tooltip().settings(settings)(selection); + selection.style("fill", d => color(d.key)); + }); + + return fc + .autoBandwidth(minBandwidth(series)) + .crossValue(d => d.crossValue) + .mainValue(d => d.mainValue) + .baseValue(d => d.baseValue); +} + +const minBandwidth = adaptee => { + const min = arg => { + return adaptee(arg); + }; + + fc.rebindAll(min, adaptee); + + min.bandwidth = (...args) => { + if (!args.length) { + return adaptee.bandwidth(); + } + adaptee.bandwidth(Math.max(args[0], 1)); + return min; + }; + + return min; +}; diff --git a/packages/perspective-viewer-d3fc/src/js/series/colorStyles.js b/packages/perspective-viewer-d3fc/src/js/series/colorStyles.js index e23a8ea106..938f318bd3 100644 --- a/packages/perspective-viewer-d3fc/src/js/series/colorStyles.js +++ b/packages/perspective-viewer-d3fc/src/js/series/colorStyles.js @@ -1,71 +1,71 @@ -/****************************************************************************** - * - * Copyright (c) 2017, the Perspective Authors. - * - * This file is part of the Perspective library, distributed under the terms of - * the Apache License 2.0. The full license can be found in the LICENSE file. - * - */ - -import * as d3 from "d3"; -import * as gparser from "gradient-parser"; - -export const initialiseStyles = (container, settings) => { - if (!settings.colorStyles) { - const data = ["series"]; - for (let n = 1; n <= 10; n++) { - data.push(`series-${n}`); - } - - const styles = { - scheme: [], - gradient: {}, - interpolator: {}, - grid: {} - }; - - const computed = computedStyle(container); - data.forEach((d, i) => { - styles[d] = computed(`--d3fc-${d}`); - - if (i > 0) { - styles.scheme.push(styles[d]); - } - }); - - styles.opacity = getOpacityFromColor(styles.series); - styles.grid.gridLineColor = computed`--d3fc-gridline--color`; - - const gradients = ["full", "positive", "negative"]; - gradients.forEach(g => { - const gradient = computed(`--d3fc-${g}--gradient`); - styles.gradient[g] = parseGradient(gradient, styles.opacity); - }); - - settings.colorStyles = styles; - } -}; - -const getOpacityFromColor = color => { - return d3.color(color).opacity; -}; - -const stepAsColor = (value, opacity) => { - const color = d3.color(`#${value}`); - color.opacity = opacity; - return color + ""; -}; - -const computedStyle = container => { - if (window.ShadyCSS) { - return d => window.ShadyCSS.getComputedStyleValue(container, d); - } else { - const containerStyles = getComputedStyle(container); - return d => containerStyles.getPropertyValue(d); - } -}; - -const parseGradient = (gradient, opacity) => { - const parsed = gparser.parse(gradient)[0].colorStops; - return parsed.map((g, i) => [g.length ? g.length.value / 100 : i / (parsed.length - 1), stepAsColor(g.value, opacity)]).sort((a, b) => a[0] - b[0]); -}; +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ + +import * as d3 from "d3"; +import * as gparser from "gradient-parser"; + +export const initialiseStyles = (container, settings) => { + if (!settings.colorStyles) { + const data = ["series"]; + for (let n = 1; n <= 10; n++) { + data.push(`series-${n}`); + } + + const styles = { + scheme: [], + gradient: {}, + interpolator: {}, + grid: {} + }; + + const computed = computedStyle(container); + data.forEach((d, i) => { + styles[d] = computed(`--d3fc-${d}`); + + if (i > 0) { + styles.scheme.push(styles[d]); + } + }); + + styles.opacity = getOpacityFromColor(styles.series); + styles.grid.gridLineColor = computed`--d3fc-gridline--color`; + + const gradients = ["full", "positive", "negative"]; + gradients.forEach(g => { + const gradient = computed(`--d3fc-${g}--gradient`); + styles.gradient[g] = parseGradient(gradient, styles.opacity); + }); + + settings.colorStyles = styles; + } +}; + +const getOpacityFromColor = color => { + return d3.color(color).opacity; +}; + +const stepAsColor = (value, opacity) => { + const color = d3.color(`#${value}`); + color.opacity = opacity; + return color + ""; +}; + +const computedStyle = container => { + if (window.ShadyCSS) { + return d => window.ShadyCSS.getComputedStyleValue(container, d); + } else { + const containerStyles = getComputedStyle(container); + return d => containerStyles.getPropertyValue(d); + } +}; + +const parseGradient = (gradient, opacity) => { + const parsed = gparser.parse(gradient)[0].colorStops; + return parsed.map((g, i) => [g.length ? g.length.value / 100 : i / (parsed.length - 1), stepAsColor(g.value, opacity)]).sort((a, b) => a[0] - b[0]); +}; diff --git a/packages/perspective-viewer-d3fc/src/js/series/lineSeries.js b/packages/perspective-viewer-d3fc/src/js/series/lineSeries.js index d515ab1b9e..ccbd553236 100644 --- a/packages/perspective-viewer-d3fc/src/js/series/lineSeries.js +++ b/packages/perspective-viewer-d3fc/src/js/series/lineSeries.js @@ -1,20 +1,20 @@ -/****************************************************************************** - * - * Copyright (c) 2017, the Perspective Authors. - * - * This file is part of the Perspective library, distributed under the terms of - * the Apache License 2.0. The full license can be found in the LICENSE file. - * - */ -import * as fc from "d3fc"; -import {withoutOpacity} from "./seriesColors.js"; - -export function lineSeries(settings, color) { - let series = fc.seriesSvgLine(); - - series = series.decorate(selection => { - selection.style("stroke", d => withoutOpacity(color(d[0] && d[0].key))); - }); - - return series.crossValue(d => d.crossValue).mainValue(d => d.mainValue); -} +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ +import * as fc from "d3fc"; +import {withoutOpacity} from "./seriesColors.js"; + +export function lineSeries(settings, color) { + let series = fc.seriesSvgLine(); + + series = series.decorate(selection => { + selection.style("stroke", d => withoutOpacity(color(d[0] && d[0].key))); + }); + + return series.crossValue(d => d.crossValue).mainValue(d => d.mainValue); +} diff --git a/packages/perspective-viewer-d3fc/src/js/series/ohlcCandleSeries.js b/packages/perspective-viewer-d3fc/src/js/series/ohlcCandleSeries.js index 11d00d748a..eea2bd6308 100644 --- a/packages/perspective-viewer-d3fc/src/js/series/ohlcCandleSeries.js +++ b/packages/perspective-viewer-d3fc/src/js/series/ohlcCandleSeries.js @@ -1,55 +1,55 @@ -/****************************************************************************** - * - * Copyright (c) 2017, the Perspective Authors. - * - * This file is part of the Perspective library, distributed under the terms of - * the Apache License 2.0. The full license can be found in the LICENSE file. - * - */ -import * as fc from "d3fc"; -import {colorScale, setOpacity} from "../series/seriesColors"; - -const isUp = d => d.closeValue >= d.openValue; - -export function ohlcCandleSeries(settings, seriesCanvas, upColor) { - const domain = upColor.domain(); - const downColor = colorScale() - .domain(domain) - .settings(settings) - .defaultColors([settings.colorStyles["series-2"]]) - .mapFunction(setOpacity(0.5))(); - const avgColor = colorScale() - .settings(settings) - .domain(domain)(); - - const series = seriesCanvas() - .crossValue(d => d.crossValue) - .openValue(d => d.openValue) - .highValue(d => d.highValue) - .lowValue(d => d.lowValue) - .closeValue(d => d.closeValue) - .decorate((context, d) => { - const color = isUp(d) ? upColor(d.key) : downColor(d.key); - context.fillStyle = color; - context.strokeStyle = color; - }); - - const bollingerAverageSeries = fc - .seriesCanvasLine() - .mainValue(d => d.bollinger.average) - .crossValue(d => d.crossValue) - .decorate((context, d) => { - context.strokeStyle = avgColor(d[0].key); - }); - - const bollingerAreaSeries = fc - .seriesCanvasArea() - .mainValue(d => d.bollinger.upper) - .baseValue(d => d.bollinger.lower) - .crossValue(d => d.crossValue) - .decorate((context, d) => { - context.fillStyle = setOpacity(0.25)(avgColor(d[0].key)); - }); - - return fc.seriesCanvasMulti().series([bollingerAreaSeries, series, bollingerAverageSeries]); -} +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ +import * as fc from "d3fc"; +import {colorScale, setOpacity} from "../series/seriesColors"; + +const isUp = d => d.closeValue >= d.openValue; + +export function ohlcCandleSeries(settings, seriesCanvas, upColor) { + const domain = upColor.domain(); + const downColor = colorScale() + .domain(domain) + .settings(settings) + .defaultColors([settings.colorStyles["series-2"]]) + .mapFunction(setOpacity(0.5))(); + const avgColor = colorScale() + .settings(settings) + .domain(domain)(); + + const series = seriesCanvas() + .crossValue(d => d.crossValue) + .openValue(d => d.openValue) + .highValue(d => d.highValue) + .lowValue(d => d.lowValue) + .closeValue(d => d.closeValue) + .decorate((context, d) => { + const color = isUp(d) ? upColor(d.key) : downColor(d.key); + context.fillStyle = color; + context.strokeStyle = color; + }); + + const bollingerAverageSeries = fc + .seriesCanvasLine() + .mainValue(d => d.bollinger.average) + .crossValue(d => d.crossValue) + .decorate((context, d) => { + context.strokeStyle = avgColor(d[0].key); + }); + + const bollingerAreaSeries = fc + .seriesCanvasArea() + .mainValue(d => d.bollinger.upper) + .baseValue(d => d.bollinger.lower) + .crossValue(d => d.crossValue) + .decorate((context, d) => { + context.fillStyle = setOpacity(0.25)(avgColor(d[0].key)); + }); + + return fc.seriesCanvasMulti().series([bollingerAreaSeries, series, bollingerAverageSeries]); +} diff --git a/packages/perspective-viewer-d3fc/src/js/series/seriesKey.js b/packages/perspective-viewer-d3fc/src/js/series/seriesKey.js index 349d910fb0..b2cf5d8edf 100644 --- a/packages/perspective-viewer-d3fc/src/js/series/seriesKey.js +++ b/packages/perspective-viewer-d3fc/src/js/series/seriesKey.js @@ -1,15 +1,15 @@ -/****************************************************************************** - * - * Copyright (c) 2017, the Perspective Authors. - * - * This file is part of the Perspective library, distributed under the terms of - * the Apache License 2.0. The full license can be found in the LICENSE file. - * - */ - -export function groupFromKey(key) { - return key - .split("|") - .slice(0, -1) - .join("|"); -} +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ + +export function groupFromKey(key) { + return key + .split("|") + .slice(0, -1) + .join("|"); +} diff --git a/packages/perspective-viewer-d3fc/src/js/series/seriesRange.js b/packages/perspective-viewer-d3fc/src/js/series/seriesRange.js index 2678481d1b..a849121826 100644 --- a/packages/perspective-viewer-d3fc/src/js/series/seriesRange.js +++ b/packages/perspective-viewer-d3fc/src/js/series/seriesRange.js @@ -1,61 +1,61 @@ -/****************************************************************************** - * - * Copyright (c) 2017, the Perspective Authors. - * - * This file is part of the Perspective library, distributed under the terms of - * the Apache License 2.0. The full license can be found in the LICENSE file. - * - */ -import * as d3 from "d3"; -import {domain} from "../axis/linearAxis"; - -export function seriesLinearRange(settings, data, valueName, customExtent) { - return d3.scaleLinear().domain(getExtent(data, valueName, customExtent)); -} - -export function seriesColorRange(settings, data, valueName, customExtent) { - let extent = getExtent(data, valueName, customExtent); - let gradient = settings.colorStyles.gradient.full; - - if (extent[0] >= 0) { - gradient = settings.colorStyles.gradient.positive; - } else if (extent[1] <= 0) { - gradient = settings.colorStyles.gradient.negative; - } else { - const maxVal = Math.max(-extent[0], extent[1]); - extent = [-maxVal, maxVal]; - } - - const interpolator = multiInterpolator(gradient); - return d3.scaleSequential(interpolator).domain(extent); -} - -const getExtent = (data, valueName, customExtent) => { - return ( - customExtent || - domain() - .valueName(valueName) - .pad([0, 0])(data) - ); -}; - -const multiInterpolator = gradientPairs => { - // A new interpolator that calls through to a set of - // interpolators between each value/color pair - const interpolators = gradientPairs.slice(1).map((p, i) => d3.interpolate(gradientPairs[i][1], p[1])); - return value => { - const index = gradientPairs.findIndex((p, i) => i < gradientPairs.length - 1 && value <= gradientPairs[i + 1][0] && value > p[0]); - if (index === -1) { - if (value <= gradientPairs[0][0]) { - return gradientPairs[0][1]; - } - return gradientPairs[gradientPairs.length - 1][1]; - } - - const interpolator = interpolators[index]; - const [value1] = gradientPairs[index]; - const [value2] = gradientPairs[index + 1]; - - return interpolator((value - value1) / (value2 - value1)); - }; -}; +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ +import * as d3 from "d3"; +import {domain} from "../axis/linearAxis"; + +export function seriesLinearRange(settings, data, valueName, customExtent) { + return d3.scaleLinear().domain(getExtent(data, valueName, customExtent)); +} + +export function seriesColorRange(settings, data, valueName, customExtent) { + let extent = getExtent(data, valueName, customExtent); + let gradient = settings.colorStyles.gradient.full; + + if (extent[0] >= 0) { + gradient = settings.colorStyles.gradient.positive; + } else if (extent[1] <= 0) { + gradient = settings.colorStyles.gradient.negative; + } else { + const maxVal = Math.max(-extent[0], extent[1]); + extent = [-maxVal, maxVal]; + } + + const interpolator = multiInterpolator(gradient); + return d3.scaleSequential(interpolator).domain(extent); +} + +const getExtent = (data, valueName, customExtent) => { + return ( + customExtent || + domain() + .valueName(valueName) + .pad([0, 0])(data) + ); +}; + +const multiInterpolator = gradientPairs => { + // A new interpolator that calls through to a set of + // interpolators between each value/color pair + const interpolators = gradientPairs.slice(1).map((p, i) => d3.interpolate(gradientPairs[i][1], p[1])); + return value => { + const index = gradientPairs.findIndex((p, i) => i < gradientPairs.length - 1 && value <= gradientPairs[i + 1][0] && value > p[0]); + if (index === -1) { + if (value <= gradientPairs[0][0]) { + return gradientPairs[0][1]; + } + return gradientPairs[gradientPairs.length - 1][1]; + } + + const interpolator = interpolators[index]; + const [value1] = gradientPairs[index]; + const [value2] = gradientPairs[index + 1]; + + return interpolator((value - value1) / (value2 - value1)); + }; +}; diff --git a/packages/perspective-viewer-d3fc/src/js/tooltip/nearbyTip.js b/packages/perspective-viewer-d3fc/src/js/tooltip/nearbyTip.js index 5c5fcea5f9..deffb13acf 100644 --- a/packages/perspective-viewer-d3fc/src/js/tooltip/nearbyTip.js +++ b/packages/perspective-viewer-d3fc/src/js/tooltip/nearbyTip.js @@ -1,168 +1,168 @@ -/****************************************************************************** - * - * Copyright (c) 2017, the Perspective Authors. - * - * This file is part of the Perspective library, distributed under the terms of - * the Apache License 2.0. The full license can be found in the LICENSE file. - * - */ - -import * as fc from "d3fc"; -import {tooltip} from "./tooltip"; -import {withOpacity} from "../series/seriesColors.js"; -import {findBestFromData} from "../data/findBest"; -import {raiseEvent} from "./selectionEvent"; - -export default () => { - const base = tooltip().alwaysShow(true); - - let xScale = null; - let yScale = null; - let color = null; - let size = null; - let canvas = false; - let data = null; - let xValueName = "crossValue"; - let yValueName = "mainValue"; - let altDataWithScale = null; - - function nearbyTip(selection) { - const chartPlotArea = `d3fc-${canvas ? "canvas" : "svg"}.plot-area`; - if (xScale || yScale) { - let tooltipData = null; - const pointer = fc.pointer().on("point", event => { - const closest = event.length ? getClosestDataPoint(event[0]) : null; - tooltipData = closest ? [closest.data] : []; - const useYScale = closest ? closest.scale : yScale; - - renderTip(selection, tooltipData, useYScale); - }); - - selection - .select(chartPlotArea) - .on("measure.nearbyTip", () => renderTip(selection, [])) - .on("click", () => { - if (tooltipData.length) { - raiseEvent(selection.node(), tooltipData[0], base.settings()); - } - }) - .call(pointer); - } - } - - const renderTip = (selection, tipData, useYScale = yScale) => { - const tips = selection - .select("d3fc-svg.plot-area svg") - .selectAll("circle.nearbyTip") - .data(tipData); - tips.exit().remove(); - - tips.enter() - .append("circle") - .attr("class", "nearbyTip") - .merge(tips) - .attr("r", d => (size ? Math.sqrt(size(d.size)) : 10)) - .attr("transform", d => `translate(${xScale(d[xValueName])},${useYScale(d[yValueName])})`) - .style("stroke", "none") - .style("fill", d => color && withOpacity(color(d.key))); - - base(tips); - }; - - const getClosestDataPoint = pos => { - const distFn = scale => { - return v => { - if (v[yValueName] === undefined || v[yValueName] === null || v[xValueName] === undefined || v[xValueName] === null) return null; - - return Math.sqrt(Math.pow(xScale(v[xValueName]) - pos.x, 2) + Math.pow(scale(v[yValueName]) - pos.y, 2)); - }; - }; - - // Check the main data - const dist1 = distFn(yScale); - const best1 = findBestFromData(data, dist1, Math.min); - - if (altDataWithScale) { - // Check the alt data with its scale, to see if any are closer - const dist2 = distFn(altDataWithScale.yScale); - const best2 = findBestFromData(altDataWithScale.data, dist2, Math.min); - return dist1(best1) <= dist2(best2) ? {data: best1, scale: yScale} : {data: best2, scale: altDataWithScale.yScale}; - } - return {data: best1, scale: yScale}; - }; - - nearbyTip.xScale = (...args) => { - if (!args.length) { - return xScale; - } - xScale = args[0]; - return nearbyTip; - }; - - nearbyTip.yScale = (...args) => { - if (!args.length) { - return yScale; - } - yScale = args[0]; - return nearbyTip; - }; - - nearbyTip.color = (...args) => { - if (!args.length) { - return color; - } - color = args[0]; - return nearbyTip; - }; - - nearbyTip.size = (...args) => { - if (!args.length) { - return size; - } - size = args[0] ? args[0].copy().range([40, 4000]) : null; - return nearbyTip; - }; - - nearbyTip.canvas = (...args) => { - if (!args.length) { - return canvas; - } - canvas = args[0]; - return nearbyTip; - }; - - nearbyTip.data = (...args) => { - if (!args.length) { - return data; - } - data = args[0]; - return nearbyTip; - }; - - nearbyTip.xValueName = (...args) => { - if (!args.length) { - return xValueName; - } - xValueName = args[0]; - return nearbyTip; - }; - - nearbyTip.yValueName = (...args) => { - if (!args.length) { - return yValueName; - } - yValueName = args[0]; - return nearbyTip; - }; - - nearbyTip.altDataWithScale = (...args) => { - if (!args.length) { - return altDataWithScale; - } - altDataWithScale = args[0]; - return nearbyTip; - }; - - fc.rebindAll(nearbyTip, base); - return nearbyTip; -}; +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ + +import * as fc from "d3fc"; +import {tooltip} from "./tooltip"; +import {withOpacity} from "../series/seriesColors.js"; +import {findBestFromData} from "../data/findBest"; +import {raiseEvent} from "./selectionEvent"; + +export default () => { + const base = tooltip().alwaysShow(true); + + let xScale = null; + let yScale = null; + let color = null; + let size = null; + let canvas = false; + let data = null; + let xValueName = "crossValue"; + let yValueName = "mainValue"; + let altDataWithScale = null; + + function nearbyTip(selection) { + const chartPlotArea = `d3fc-${canvas ? "canvas" : "svg"}.plot-area`; + if (xScale || yScale) { + let tooltipData = null; + const pointer = fc.pointer().on("point", event => { + const closest = event.length ? getClosestDataPoint(event[0]) : null; + tooltipData = closest ? [closest.data] : []; + const useYScale = closest ? closest.scale : yScale; + + renderTip(selection, tooltipData, useYScale); + }); + + selection + .select(chartPlotArea) + .on("measure.nearbyTip", () => renderTip(selection, [])) + .on("click", () => { + if (tooltipData.length) { + raiseEvent(selection.node(), tooltipData[0], base.settings()); + } + }) + .call(pointer); + } + } + + const renderTip = (selection, tipData, useYScale = yScale) => { + const tips = selection + .select("d3fc-svg.plot-area svg") + .selectAll("circle.nearbyTip") + .data(tipData); + tips.exit().remove(); + + tips.enter() + .append("circle") + .attr("class", "nearbyTip") + .merge(tips) + .attr("r", d => (size ? Math.sqrt(size(d.size)) : 10)) + .attr("transform", d => `translate(${xScale(d[xValueName])},${useYScale(d[yValueName])})`) + .style("stroke", "none") + .style("fill", d => color && withOpacity(color(d.key))); + + base(tips); + }; + + const getClosestDataPoint = pos => { + const distFn = scale => { + return v => { + if (v[yValueName] === undefined || v[yValueName] === null || v[xValueName] === undefined || v[xValueName] === null) return null; + + return Math.sqrt(Math.pow(xScale(v[xValueName]) - pos.x, 2) + Math.pow(scale(v[yValueName]) - pos.y, 2)); + }; + }; + + // Check the main data + const dist1 = distFn(yScale); + const best1 = findBestFromData(data, dist1, Math.min); + + if (altDataWithScale) { + // Check the alt data with its scale, to see if any are closer + const dist2 = distFn(altDataWithScale.yScale); + const best2 = findBestFromData(altDataWithScale.data, dist2, Math.min); + return dist1(best1) <= dist2(best2) ? {data: best1, scale: yScale} : {data: best2, scale: altDataWithScale.yScale}; + } + return {data: best1, scale: yScale}; + }; + + nearbyTip.xScale = (...args) => { + if (!args.length) { + return xScale; + } + xScale = args[0]; + return nearbyTip; + }; + + nearbyTip.yScale = (...args) => { + if (!args.length) { + return yScale; + } + yScale = args[0]; + return nearbyTip; + }; + + nearbyTip.color = (...args) => { + if (!args.length) { + return color; + } + color = args[0]; + return nearbyTip; + }; + + nearbyTip.size = (...args) => { + if (!args.length) { + return size; + } + size = args[0] ? args[0].copy().range([40, 4000]) : null; + return nearbyTip; + }; + + nearbyTip.canvas = (...args) => { + if (!args.length) { + return canvas; + } + canvas = args[0]; + return nearbyTip; + }; + + nearbyTip.data = (...args) => { + if (!args.length) { + return data; + } + data = args[0]; + return nearbyTip; + }; + + nearbyTip.xValueName = (...args) => { + if (!args.length) { + return xValueName; + } + xValueName = args[0]; + return nearbyTip; + }; + + nearbyTip.yValueName = (...args) => { + if (!args.length) { + return yValueName; + } + yValueName = args[0]; + return nearbyTip; + }; + + nearbyTip.altDataWithScale = (...args) => { + if (!args.length) { + return altDataWithScale; + } + altDataWithScale = args[0]; + return nearbyTip; + }; + + fc.rebindAll(nearbyTip, base); + return nearbyTip; +}; diff --git a/packages/perspective-viewer-d3fc/src/js/tooltip/selectionData.js b/packages/perspective-viewer-d3fc/src/js/tooltip/selectionData.js index d15aea7de1..face58878d 100644 --- a/packages/perspective-viewer-d3fc/src/js/tooltip/selectionData.js +++ b/packages/perspective-viewer-d3fc/src/js/tooltip/selectionData.js @@ -1,53 +1,53 @@ -/****************************************************************************** - * - * Copyright (c) 2017, the Perspective Authors. - * - * This file is part of the Perspective library, distributed under the terms of - * the Apache License 2.0. The full license can be found in the LICENSE file. - * - */ - -function toValue(type, value) { - switch (type) { - case "date": - case "datetime": - return value instanceof Date ? value : new Date(parseInt(value)); - case "integer": - return parseInt(value, 10); - case "float": - return parseFloat(value); - } - return value; -} - -export function getGroupValues(data, settings) { - if (settings.crossValues.length === 0) return []; - const groupValues = (data.crossValue.split ? data.crossValue.split("|") : [data.crossValue]) || [data.key]; - return groupValues.map((cross, i) => ({name: settings.crossValues[i].name, value: toValue(settings.crossValues[i].type, cross)})); -} - -export function getSplitValues(data, settings) { - if (settings.splitValues.length === 0) return []; - const splitValues = data.key ? data.key.split("|") : data.mainValue.split ? data.mainValue.split("|") : [data.mainValue]; - return settings.splitValues.map((split, i) => ({name: split.name, value: toValue(split.type, splitValues[i])})); -} - -export function getDataValues(data, settings) { - if (settings.mainValues.length > 1) { - if (data.mainValue) { - return [ - { - name: data.key, - value: data.mainValue - (data.baseValue || 0) - } - ]; - } - return settings.mainValues.map((main, i) => ({name: main.name, value: toValue(main.type, data.mainValues[i])})); - } - return [ - { - name: settings.mainValues[0].name, - value: toValue(settings.mainValues[0].type, data.colorValue || data.mainValue - data.baseValue || data.mainValue || data.mainValues) - } - ]; -} +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ + +function toValue(type, value) { + switch (type) { + case "date": + case "datetime": + return value instanceof Date ? value : new Date(parseInt(value)); + case "integer": + return parseInt(value, 10); + case "float": + return parseFloat(value); + } + return value; +} + +export function getGroupValues(data, settings) { + if (settings.crossValues.length === 0) return []; + const groupValues = (data.crossValue.split ? data.crossValue.split("|") : [data.crossValue]) || [data.key]; + return groupValues.map((cross, i) => ({name: settings.crossValues[i].name, value: toValue(settings.crossValues[i].type, cross)})); +} + +export function getSplitValues(data, settings) { + if (settings.splitValues.length === 0) return []; + const splitValues = data.key ? data.key.split("|") : data.mainValue.split ? data.mainValue.split("|") : [data.mainValue]; + return settings.splitValues.map((split, i) => ({name: split.name, value: toValue(split.type, splitValues[i])})); +} + +export function getDataValues(data, settings) { + if (settings.mainValues.length > 1) { + if (data.mainValue) { + return [ + { + name: data.key, + value: data.mainValue - (data.baseValue || 0) + } + ]; + } + return settings.mainValues.map((main, i) => ({name: main.name, value: toValue(main.type, data.mainValues[i])})); + } + return [ + { + name: settings.mainValues[0].name, + value: toValue(settings.mainValues[0].type, data.colorValue || data.mainValue - data.baseValue || data.mainValue || data.mainValues) + } + ]; +} diff --git a/packages/perspective-viewer-d3fc/src/js/tooltip/selectionEvent.js b/packages/perspective-viewer-d3fc/src/js/tooltip/selectionEvent.js index d7c443e5e7..0c2283e3be 100644 --- a/packages/perspective-viewer-d3fc/src/js/tooltip/selectionEvent.js +++ b/packages/perspective-viewer-d3fc/src/js/tooltip/selectionEvent.js @@ -1,50 +1,50 @@ -/****************************************************************************** - * - * Copyright (c) 2017, the Perspective Authors. - * - * This file is part of the Perspective library, distributed under the terms of - * the Apache License 2.0. The full license can be found in the LICENSE file. - * - */ -import {getGroupValues, getSplitValues, getDataValues} from "./selectionData"; - -const mapToFilter = d => [d.name, "==", d.value]; - -export const raiseEvent = (node, data, settings) => { - const column_names = getDataValues(data, settings).map(d => d.name); - const groupFilters = getGroupValues(data, settings).map(mapToFilter); - const splitFilters = getSplitValues(data, settings).map(mapToFilter); - - const filters = settings.filter.concat(groupFilters).concat(splitFilters); - - node.dispatchEvent( - new CustomEvent("perspective-click", { - bubbles: true, - composed: true, - detail: { - column_names, - config: {filters}, - row: data.row - } - }) - ); -}; - -export const selectionEvent = () => { - let settings = null; - - const _event = selection => { - const node = selection.node(); - selection.on("click", data => raiseEvent(node, data, settings)); - }; - - _event.settings = (...args) => { - if (!args.length) { - return settings; - } - settings = args[0]; - return _event; - }; - - return _event; -}; +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ +import {getGroupValues, getSplitValues, getDataValues} from "./selectionData"; + +const mapToFilter = d => [d.name, "==", d.value]; + +export const raiseEvent = (node, data, settings) => { + const column_names = getDataValues(data, settings).map(d => d.name); + const groupFilters = getGroupValues(data, settings).map(mapToFilter); + const splitFilters = getSplitValues(data, settings).map(mapToFilter); + + const filters = settings.filter.concat(groupFilters).concat(splitFilters); + + node.dispatchEvent( + new CustomEvent("perspective-click", { + bubbles: true, + composed: true, + detail: { + column_names, + config: {filters}, + row: data.row + } + }) + ); +}; + +export const selectionEvent = () => { + let settings = null; + + const _event = selection => { + const node = selection.node(); + selection.on("click", data => raiseEvent(node, data, settings)); + }; + + _event.settings = (...args) => { + if (!args.length) { + return settings; + } + settings = args[0]; + return _event; + }; + + return _event; +}; diff --git a/packages/perspective-viewer-d3fc/src/js/zoom/zoomableChart.js b/packages/perspective-viewer-d3fc/src/js/zoom/zoomableChart.js index 60e689edb3..593752d241 100644 --- a/packages/perspective-viewer-d3fc/src/js/zoom/zoomableChart.js +++ b/packages/perspective-viewer-d3fc/src/js/zoom/zoomableChart.js @@ -1,194 +1,194 @@ -/****************************************************************************** - * - * Copyright (c) 2017, the Perspective Authors. - * - * This file is part of the Perspective library, distributed under the terms of - * the Apache License 2.0. The full license can be found in the LICENSE file. - * - */ - -import * as d3 from "d3"; -import {getOrCreateElement} from "../utils/utils"; -import template from "../../html/zoom-controls.html"; - -export default () => { - let chart = null; - let settings = null; - let xScale = null; - let xCopy = null; - let yScale = null; - let yCopy = null; - let bound = false; - let canvas = false; - let onChange = () => {}; - - function zoomableChart(selection) { - const chartPlotArea = `d3fc-${canvas ? "canvas" : "svg"}.plot-area`; - if (xScale || yScale) { - const dateAxis = xCopy && xCopy.domain()[0] instanceof Date; - const zoom = d3.zoom().on("zoom", () => { - const {transform} = d3.event; - settings.zoom = { - k: transform.k, - x: transform.x, - y: transform.y - }; - - applyTransform(transform); - - selection.call(chart); - - const noZoom = transform.k === 1 && transform.x === 0 && transform.y === 0; - - const zoomControls = getZoomControls(selection).style("display", noZoom ? "none" : ""); - zoomControls.select("#zoom-reset").on("click", () => selection.select(chartPlotArea).call(zoom.transform, d3.zoomIdentity)); - - const oneYear = zoomControls.select("#one-year").style("display", dateAxis ? "" : "none"); - const sixMonths = zoomControls.select("#six-months").style("display", dateAxis ? "" : "none"); - const oneMonth = zoomControls.select("#one-month").style("display", dateAxis ? "" : "none"); - if (dateAxis) { - const dateClick = endCalculation => () => { - const start = new Date(xScale.domain()[0]); - const end = new Date(start); - endCalculation(start, end); - - const xRange = xCopy.range(); - const k = (xRange[1] - xRange[0]) / (xCopy(end) - xCopy(start)); - const x = -xCopy(start) * k; - let y = 0; - if (yScale) { - const yMiddle = yScale.domain().reduce((a, b) => a + b) / 2; - y = -yCopy(yMiddle) * k + yScale(yMiddle); - } - selection.select(chartPlotArea).call(zoom.transform, d3.zoomIdentity.translate(x, y).scale(k)); - }; - - oneYear.on( - "click", - dateClick((start, end) => end.setYear(start.getFullYear() + 1)) - ); - sixMonths.on( - "click", - dateClick((start, end) => end.setMonth(start.getMonth() + 6)) - ); - oneMonth.on( - "click", - dateClick((start, end) => end.setMonth(start.getMonth() + 1)) - ); - } - }); - - const oldDecorate = chart.decorate(); - chart.decorate((sel, data) => { - oldDecorate(sel, data); - if (!bound) { - bound = true; - // add the zoom interaction on the enter selection - const plotArea = sel.select(chartPlotArea); - - plotArea - .on("measure.zoom-range", () => { - if (xCopy) xCopy.range([0, d3.event.detail.width]); - if (yCopy) yCopy.range([0, d3.event.detail.height]); - - if (settings.zoom) { - const initialTransform = d3.zoomIdentity.translate(settings.zoom.x, settings.zoom.y).scale(settings.zoom.k); - plotArea.call(zoom.transform, initialTransform); - } - }) - .call(zoom); - } - }); - } - - selection.call(chart); - } - - zoomableChart.chart = (...args) => { - if (!args.length) { - return chart; - } - chart = args[0]; - return zoomableChart; - }; - - zoomableChart.settings = (...args) => { - if (!args.length) { - return settings; - } - settings = args[0]; - return zoomableChart; - }; - - zoomableChart.xScale = (...args) => { - if (!args.length) { - return xScale; - } - xScale = zoomableScale(args[0]); - xCopy = xScale ? xScale.copy() : null; - return zoomableChart; - }; - - zoomableChart.yScale = (...args) => { - if (!args.length) { - return yScale; - } - yScale = zoomableScale(args[0]); - yCopy = yScale ? yScale.copy() : null; - if (yCopy) { - const yDomain = yCopy.domain(); - yCopy.domain([yDomain[1], yDomain[0]]); - } - return zoomableChart; - }; - - zoomableChart.canvas = (...args) => { - if (!args.length) { - return canvas; - } - canvas = args[0]; - return zoomableChart; - }; - - zoomableChart.onChange = (...args) => { - if (!args.length) { - return onChange; - } - onChange = args[0]; - return zoomableChart; - }; - - const applyTransform = transform => { - const changeArgs = {...transform}; - if (xScale) { - xScale.domain(transform.rescaleX(xCopy).domain()); - changeArgs.xDomain = xScale.domain(); - } - - if (yScale) { - const yZoomDomain = transform.rescaleY(yCopy).domain(); - yScale.domain([yZoomDomain[1], yZoomDomain[0]]); - changeArgs.yDomain = yScale.domain(); - } - - onChange(changeArgs); - }; - - const getZoomControls = container => - getOrCreateElement(container, ".zoom-controls", () => - container - .append("div") - .attr("class", "zoom-controls") - .style("display", "none") - .html(template) - ); - - const zoomableScale = scale => { - if (scale && scale.nice) { - return scale; - } - return null; - }; - - return zoomableChart; -}; +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ + +import * as d3 from "d3"; +import {getOrCreateElement} from "../utils/utils"; +import template from "../../html/zoom-controls.html"; + +export default () => { + let chart = null; + let settings = null; + let xScale = null; + let xCopy = null; + let yScale = null; + let yCopy = null; + let bound = false; + let canvas = false; + let onChange = () => {}; + + function zoomableChart(selection) { + const chartPlotArea = `d3fc-${canvas ? "canvas" : "svg"}.plot-area`; + if (xScale || yScale) { + const dateAxis = xCopy && xCopy.domain()[0] instanceof Date; + const zoom = d3.zoom().on("zoom", () => { + const {transform} = d3.event; + settings.zoom = { + k: transform.k, + x: transform.x, + y: transform.y + }; + + applyTransform(transform); + + selection.call(chart); + + const noZoom = transform.k === 1 && transform.x === 0 && transform.y === 0; + + const zoomControls = getZoomControls(selection).style("display", noZoom ? "none" : ""); + zoomControls.select("#zoom-reset").on("click", () => selection.select(chartPlotArea).call(zoom.transform, d3.zoomIdentity)); + + const oneYear = zoomControls.select("#one-year").style("display", dateAxis ? "" : "none"); + const sixMonths = zoomControls.select("#six-months").style("display", dateAxis ? "" : "none"); + const oneMonth = zoomControls.select("#one-month").style("display", dateAxis ? "" : "none"); + if (dateAxis) { + const dateClick = endCalculation => () => { + const start = new Date(xScale.domain()[0]); + const end = new Date(start); + endCalculation(start, end); + + const xRange = xCopy.range(); + const k = (xRange[1] - xRange[0]) / (xCopy(end) - xCopy(start)); + const x = -xCopy(start) * k; + let y = 0; + if (yScale) { + const yMiddle = yScale.domain().reduce((a, b) => a + b) / 2; + y = -yCopy(yMiddle) * k + yScale(yMiddle); + } + selection.select(chartPlotArea).call(zoom.transform, d3.zoomIdentity.translate(x, y).scale(k)); + }; + + oneYear.on( + "click", + dateClick((start, end) => end.setYear(start.getFullYear() + 1)) + ); + sixMonths.on( + "click", + dateClick((start, end) => end.setMonth(start.getMonth() + 6)) + ); + oneMonth.on( + "click", + dateClick((start, end) => end.setMonth(start.getMonth() + 1)) + ); + } + }); + + const oldDecorate = chart.decorate(); + chart.decorate((sel, data) => { + oldDecorate(sel, data); + if (!bound) { + bound = true; + // add the zoom interaction on the enter selection + const plotArea = sel.select(chartPlotArea); + + plotArea + .on("measure.zoom-range", () => { + if (xCopy) xCopy.range([0, d3.event.detail.width]); + if (yCopy) yCopy.range([0, d3.event.detail.height]); + + if (settings.zoom) { + const initialTransform = d3.zoomIdentity.translate(settings.zoom.x, settings.zoom.y).scale(settings.zoom.k); + plotArea.call(zoom.transform, initialTransform); + } + }) + .call(zoom); + } + }); + } + + selection.call(chart); + } + + zoomableChart.chart = (...args) => { + if (!args.length) { + return chart; + } + chart = args[0]; + return zoomableChart; + }; + + zoomableChart.settings = (...args) => { + if (!args.length) { + return settings; + } + settings = args[0]; + return zoomableChart; + }; + + zoomableChart.xScale = (...args) => { + if (!args.length) { + return xScale; + } + xScale = zoomableScale(args[0]); + xCopy = xScale ? xScale.copy() : null; + return zoomableChart; + }; + + zoomableChart.yScale = (...args) => { + if (!args.length) { + return yScale; + } + yScale = zoomableScale(args[0]); + yCopy = yScale ? yScale.copy() : null; + if (yCopy) { + const yDomain = yCopy.domain(); + yCopy.domain([yDomain[1], yDomain[0]]); + } + return zoomableChart; + }; + + zoomableChart.canvas = (...args) => { + if (!args.length) { + return canvas; + } + canvas = args[0]; + return zoomableChart; + }; + + zoomableChart.onChange = (...args) => { + if (!args.length) { + return onChange; + } + onChange = args[0]; + return zoomableChart; + }; + + const applyTransform = transform => { + const changeArgs = {...transform}; + if (xScale) { + xScale.domain(transform.rescaleX(xCopy).domain()); + changeArgs.xDomain = xScale.domain(); + } + + if (yScale) { + const yZoomDomain = transform.rescaleY(yCopy).domain(); + yScale.domain([yZoomDomain[1], yZoomDomain[0]]); + changeArgs.yDomain = yScale.domain(); + } + + onChange(changeArgs); + }; + + const getZoomControls = container => + getOrCreateElement(container, ".zoom-controls", () => + container + .append("div") + .attr("class", "zoom-controls") + .style("display", "none") + .html(template) + ); + + const zoomableScale = scale => { + if (scale && scale.nice) { + return scale; + } + return null; + }; + + return zoomableChart; +}; diff --git a/packages/perspective-viewer-d3fc/test/js/unit/series/colorStyles.spec.js b/packages/perspective-viewer-d3fc/test/js/unit/series/colorStyles.spec.js index 0f7e1f0897..1574d6d4a5 100644 --- a/packages/perspective-viewer-d3fc/test/js/unit/series/colorStyles.spec.js +++ b/packages/perspective-viewer-d3fc/test/js/unit/series/colorStyles.spec.js @@ -1,87 +1,87 @@ -/****************************************************************************** - * - * Copyright (c) 2017, the Perspective Authors. - * - * This file is part of the Perspective library, distributed under the terms of - * the Apache License 2.0. The full license can be found in the LICENSE file. - * - */ -import {select} from "d3"; -import {initialiseStyles} from "../../../../src/js/series/colorStyles"; -import * as sinon from "sinon"; - -const styleVariables = { - "--d3fc-series": "rgba(31, 119, 180, 0.5)", - "--d3fc-series-1": "#0366d6", - "--d3fc-series-2": "#ff7f0e", - "--d3fc-series-3": "#2ca02c", - "--d3fc-series-4": "#d62728", - "--d3fc-series-5": "#9467bd", - "--d3fc-series-6": "#8c564b", - "--d3fc-series-7": "#e377c2", - "--d3fc-series-8": "#7f7f7f", - "--d3fc-series-9": "#bcbd22", - "--d3fc-series-10": "#17becf", - "--d3fc-full--gradient": `linear-gradient( - #4d342f 0%, - #f0f0f0 50%, - #1a237e 100% - )`, - "--d3fc-positive--gradient": `linear-gradient( - #dcedc8 0%, - #1a237e 100% - )`, - "--d3fc-negative--gradient": `linear-gradient( - #feeb65 100%, - #4d342f 0% - )` -}; - -describe("colorStyles should", () => { - let container = null; - let settings = null; - - beforeEach(() => { - container = select("body").append("div"); - - settings = {}; - - window.ShadyCSS = { - getComputedStyleValue: sinon.spy((e, d) => styleVariables[d]) - }; - }); - - afterEach(() => { - container.remove(); - }); - - test("initialise colors from CSS variables", () => { - initialiseStyles(container.node(), settings); - const result = settings.colorStyles; - - expect(result.opacity).toEqual(0.5); - expect(result.series).toEqual(styleVariables["--d3fc-series"]); - for (let n = 1; n <= 10; n++) { - expect(result[`series-${n}`]).toEqual(styleVariables[`--d3fc-series-${n}`]); - } - }); - - test("initialise gradients with opacity", () => { - initialiseStyles(container.node(), settings); - const result = settings.colorStyles; - - expect(result.gradient.full).toEqual([ - [0, "rgba(77, 52, 47, 0.5)"], - [0.5, "rgba(240, 240, 240, 0.5)"], - [1, "rgba(26, 35, 126, 0.5)"] - ]); - expect(result.gradient.positive).toEqual([ - [0, "rgba(220, 237, 200, 0.5)"], - [1, "rgba(26, 35, 126, 0.5)"] - ]); - expect(result.gradient.negative).toEqual([ - [0, "rgba(77, 52, 47, 0.5)"], - [1, "rgba(254, 235, 101, 0.5)"] - ]); - }); -}); +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ +import {select} from "d3"; +import {initialiseStyles} from "../../../../src/js/series/colorStyles"; +import * as sinon from "sinon"; + +const styleVariables = { + "--d3fc-series": "rgba(31, 119, 180, 0.5)", + "--d3fc-series-1": "#0366d6", + "--d3fc-series-2": "#ff7f0e", + "--d3fc-series-3": "#2ca02c", + "--d3fc-series-4": "#d62728", + "--d3fc-series-5": "#9467bd", + "--d3fc-series-6": "#8c564b", + "--d3fc-series-7": "#e377c2", + "--d3fc-series-8": "#7f7f7f", + "--d3fc-series-9": "#bcbd22", + "--d3fc-series-10": "#17becf", + "--d3fc-full--gradient": `linear-gradient( + #4d342f 0%, + #f0f0f0 50%, + #1a237e 100% + )`, + "--d3fc-positive--gradient": `linear-gradient( + #dcedc8 0%, + #1a237e 100% + )`, + "--d3fc-negative--gradient": `linear-gradient( + #feeb65 100%, + #4d342f 0% + )` +}; + +describe("colorStyles should", () => { + let container = null; + let settings = null; + + beforeEach(() => { + container = select("body").append("div"); + + settings = {}; + + window.ShadyCSS = { + getComputedStyleValue: sinon.spy((e, d) => styleVariables[d]) + }; + }); + + afterEach(() => { + container.remove(); + }); + + test("initialise colors from CSS variables", () => { + initialiseStyles(container.node(), settings); + const result = settings.colorStyles; + + expect(result.opacity).toEqual(0.5); + expect(result.series).toEqual(styleVariables["--d3fc-series"]); + for (let n = 1; n <= 10; n++) { + expect(result[`series-${n}`]).toEqual(styleVariables[`--d3fc-series-${n}`]); + } + }); + + test("initialise gradients with opacity", () => { + initialiseStyles(container.node(), settings); + const result = settings.colorStyles; + + expect(result.gradient.full).toEqual([ + [0, "rgba(77, 52, 47, 0.5)"], + [0.5, "rgba(240, 240, 240, 0.5)"], + [1, "rgba(26, 35, 126, 0.5)"] + ]); + expect(result.gradient.positive).toEqual([ + [0, "rgba(220, 237, 200, 0.5)"], + [1, "rgba(26, 35, 126, 0.5)"] + ]); + expect(result.gradient.negative).toEqual([ + [0, "rgba(77, 52, 47, 0.5)"], + [1, "rgba(254, 235, 101, 0.5)"] + ]); + }); +}); diff --git a/packages/perspective-viewer-d3fc/test/js/unit/series/seriesRange.spec.js b/packages/perspective-viewer-d3fc/test/js/unit/series/seriesRange.spec.js index b154092482..b6928088fa 100644 --- a/packages/perspective-viewer-d3fc/test/js/unit/series/seriesRange.spec.js +++ b/packages/perspective-viewer-d3fc/test/js/unit/series/seriesRange.spec.js @@ -1,128 +1,128 @@ -/****************************************************************************** - * - * Copyright (c) 2017, the Perspective Authors. - * - * This file is part of the Perspective library, distributed under the terms of - * the Apache License 2.0. The full license can be found in the LICENSE file. - * - */ -import * as linearAxis from "../../../../src/js/axis/linearAxis"; -import * as seriesRange from "../../../../src/js/series/seriesRange"; -import * as sinon from "sinon"; - -const settings = { - colorStyles: { - gradient: { - positive: [ - [0, "rgb(0, 0, 0)"], - [1, "rgb(100, 0, 0)"] - ], - negative: [ - [0, "rgb(0, 0, 0)"], - [1, "rgb(0, 100, 0)"] - ], - full: [ - [0, "rgb(100, 0, 0)"], - [0.5, "rgb(0, 0, 0)"], - [1, "rgb(0, 0, 100)"] - ] - } - } -}; - -describe("seriesRange", () => { - let sandbox; - let domainStub; - - beforeEach(() => { - domainStub = sinon.stub().returns([100, 1100]); - domainStub.valueName = sinon.stub().returns(domainStub); - domainStub.pad = sinon.stub().returns(domainStub); - - sandbox = sinon.createSandbox(); - sandbox.stub(linearAxis, "domain").returns(domainStub); - }); - - afterEach(() => { - sandbox.restore(); - }); - - describe("seriesLinearRange should", () => { - test("get extent from domain", () => { - const data = ["a", "b", "c"]; - seriesRange.seriesLinearRange(settings, data, "test-value"); - - sinon.assert.calledWith(domainStub.valueName, "test-value"); - sinon.assert.calledWith(domainStub.pad, [0, 0]); - sinon.assert.calledWith(domainStub, data); - }); - - test("create linear range from data extent", () => { - const result = seriesRange.seriesLinearRange(settings, [], "test-value"); - - expect(result.domain()).toEqual([100, 1100]); - result.range([0, 100]); - - expect(result(100)).toEqual(0); - expect(result(500)).toEqual(40); - expect(result(700)).toEqual(60); - expect(result(1100)).toEqual(100); - }); - - test("create linear range from custom extent", () => { - const result = seriesRange.seriesLinearRange(settings, [], "test-value", [200, 300]); - - sinon.assert.notCalled(domainStub); - expect(result.domain()).toEqual([200, 300]); - }); - }); - - describe("seriesColorRange should", () => { - test("get extent from domain", () => { - const data = ["a", "b", "c"]; - seriesRange.seriesColorRange(settings, data, "test-value"); - - sinon.assert.calledWith(domainStub.valueName, "test-value"); - sinon.assert.calledWith(domainStub.pad, [0, 0]); - sinon.assert.calledWith(domainStub, data); - }); - - test("return color range from data extent", () => { - const data = []; - const result = seriesRange.seriesColorRange(settings, data, "test-value"); - - expect(result.domain()).toEqual([100, 1100]); - - expect(result(100)).toEqual("rgb(0, 0, 0)"); - expect(result(500)).toEqual("rgb(40, 0, 0)"); - expect(result(700)).toEqual("rgb(60, 0, 0)"); - expect(result(1100)).toEqual("rgb(100, 0, 0)"); - }); - - test("create linear range from custom extent", () => { - const result = seriesRange.seriesColorRange(settings, [], "test-value", [200, 300]); - - sinon.assert.notCalled(domainStub); - expect(result.domain()).toEqual([200, 300]); - }); - - test("return negative color range from custom extent", () => { - const result = seriesRange.seriesColorRange(settings, [], "test-value", [-200, -100]); - - expect(result(-200)).toEqual("rgb(0, 0, 0)"); - expect(result(-160)).toEqual("rgb(0, 40, 0)"); - expect(result(-140)).toEqual("rgb(0, 60, 0)"); - expect(result(-100)).toEqual("rgb(0, 100, 0)"); - }); - - test("return full color range from custom extent", () => { - const result = seriesRange.seriesColorRange(settings, [], "test-value", [-100, 100]); - - expect(result(-100)).toEqual("rgb(100, 0, 0)"); - expect(result(-40)).toEqual("rgb(40, 0, 0)"); - expect(result(0)).toEqual("rgb(0, 0, 0)"); - expect(result(40)).toEqual("rgb(0, 0, 40)"); - expect(result(100)).toEqual("rgb(0, 0, 100)"); - }); - }); -}); +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ +import * as linearAxis from "../../../../src/js/axis/linearAxis"; +import * as seriesRange from "../../../../src/js/series/seriesRange"; +import * as sinon from "sinon"; + +const settings = { + colorStyles: { + gradient: { + positive: [ + [0, "rgb(0, 0, 0)"], + [1, "rgb(100, 0, 0)"] + ], + negative: [ + [0, "rgb(0, 0, 0)"], + [1, "rgb(0, 100, 0)"] + ], + full: [ + [0, "rgb(100, 0, 0)"], + [0.5, "rgb(0, 0, 0)"], + [1, "rgb(0, 0, 100)"] + ] + } + } +}; + +describe("seriesRange", () => { + let sandbox; + let domainStub; + + beforeEach(() => { + domainStub = sinon.stub().returns([100, 1100]); + domainStub.valueName = sinon.stub().returns(domainStub); + domainStub.pad = sinon.stub().returns(domainStub); + + sandbox = sinon.createSandbox(); + sandbox.stub(linearAxis, "domain").returns(domainStub); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe("seriesLinearRange should", () => { + test("get extent from domain", () => { + const data = ["a", "b", "c"]; + seriesRange.seriesLinearRange(settings, data, "test-value"); + + sinon.assert.calledWith(domainStub.valueName, "test-value"); + sinon.assert.calledWith(domainStub.pad, [0, 0]); + sinon.assert.calledWith(domainStub, data); + }); + + test("create linear range from data extent", () => { + const result = seriesRange.seriesLinearRange(settings, [], "test-value"); + + expect(result.domain()).toEqual([100, 1100]); + result.range([0, 100]); + + expect(result(100)).toEqual(0); + expect(result(500)).toEqual(40); + expect(result(700)).toEqual(60); + expect(result(1100)).toEqual(100); + }); + + test("create linear range from custom extent", () => { + const result = seriesRange.seriesLinearRange(settings, [], "test-value", [200, 300]); + + sinon.assert.notCalled(domainStub); + expect(result.domain()).toEqual([200, 300]); + }); + }); + + describe("seriesColorRange should", () => { + test("get extent from domain", () => { + const data = ["a", "b", "c"]; + seriesRange.seriesColorRange(settings, data, "test-value"); + + sinon.assert.calledWith(domainStub.valueName, "test-value"); + sinon.assert.calledWith(domainStub.pad, [0, 0]); + sinon.assert.calledWith(domainStub, data); + }); + + test("return color range from data extent", () => { + const data = []; + const result = seriesRange.seriesColorRange(settings, data, "test-value"); + + expect(result.domain()).toEqual([100, 1100]); + + expect(result(100)).toEqual("rgb(0, 0, 0)"); + expect(result(500)).toEqual("rgb(40, 0, 0)"); + expect(result(700)).toEqual("rgb(60, 0, 0)"); + expect(result(1100)).toEqual("rgb(100, 0, 0)"); + }); + + test("create linear range from custom extent", () => { + const result = seriesRange.seriesColorRange(settings, [], "test-value", [200, 300]); + + sinon.assert.notCalled(domainStub); + expect(result.domain()).toEqual([200, 300]); + }); + + test("return negative color range from custom extent", () => { + const result = seriesRange.seriesColorRange(settings, [], "test-value", [-200, -100]); + + expect(result(-200)).toEqual("rgb(0, 0, 0)"); + expect(result(-160)).toEqual("rgb(0, 40, 0)"); + expect(result(-140)).toEqual("rgb(0, 60, 0)"); + expect(result(-100)).toEqual("rgb(0, 100, 0)"); + }); + + test("return full color range from custom extent", () => { + const result = seriesRange.seriesColorRange(settings, [], "test-value", [-100, 100]); + + expect(result(-100)).toEqual("rgb(100, 0, 0)"); + expect(result(-40)).toEqual("rgb(40, 0, 0)"); + expect(result(0)).toEqual("rgb(0, 0, 0)"); + expect(result(40)).toEqual("rgb(0, 0, 40)"); + expect(result(100)).toEqual("rgb(0, 0, 100)"); + }); + }); +}); diff --git a/packages/perspective-viewer-d3fc/test/js/unit/tooltip/generateHTML.spec.js b/packages/perspective-viewer-d3fc/test/js/unit/tooltip/generateHTML.spec.js index d743b64b1c..d26cf7f996 100644 --- a/packages/perspective-viewer-d3fc/test/js/unit/tooltip/generateHTML.spec.js +++ b/packages/perspective-viewer-d3fc/test/js/unit/tooltip/generateHTML.spec.js @@ -1,130 +1,130 @@ -/****************************************************************************** - * - * Copyright (c) 2017, the Perspective Authors. - * - * This file is part of the Perspective library, distributed under the terms of - * the Apache License 2.0. The full license can be found in the LICENSE file. - * - */ -import {select} from "d3"; -import {generateHtml} from "../../../../src/js/tooltip/generateHTML"; - -describe("tooltip generateHTML should", () => { - let tooltip = null; - let settings = null; - - beforeEach(() => { - tooltip = select("body") - .append("div") - .classed("tooltip-test", true); - tooltip.append("div").attr("id", "tooltip-values"); - - settings = { - crossValues: [], - splitValues: [], - mainValues: [{name: "main-1", type: "integer"}] - }; - }); - afterEach(() => { - tooltip.remove(); - }); - - const getContent = () => { - const content = []; - tooltip.selectAll("li").each((d, i, nodes) => { - content.push(select(nodes[i]).text()); - }); - return content; - }; - - test("show single mainValue", () => { - const data = { - mainValue: 101 - }; - generateHtml(tooltip, data, settings); - expect(getContent()).toEqual(["main-1: 101"]); - }); - - test("show multiple mainValues", () => { - settings.mainValues.push({name: "main-2", type: "float"}); - const data = { - mainValues: [101, 202.22] - }; - generateHtml(tooltip, data, settings); - expect(getContent()).toEqual(["main-1: 101", "main-2: 202.22"]); - }); - - test("format mainValue as date", () => { - settings.mainValues[0].type = "datetime"; - const testDate = new Date("2019-04-03T15:15Z"); - const data = { - mainValue: testDate.getTime() - }; - generateHtml(tooltip, data, settings); - expect(getContent()).toEqual([`main-1: ${testDate.toLocaleString()}`]); - }); - - test("format mainValue as integer", () => { - settings.mainValues[0].type = "integer"; - const data = { - mainValue: 12345.6789 - }; - generateHtml(tooltip, data, settings); - expect(getContent()).toEqual(["main-1: 12,345"]); - }); - - test("format mainValue as decimal", () => { - settings.mainValues[0].type = "float"; - const data = { - mainValue: 12345.6789 - }; - generateHtml(tooltip, data, settings); - expect(getContent()).toEqual(["main-1: 12,345.679"]); - }); - - test("show with single crossValue", () => { - settings.crossValues.push({name: "cross-1", type: "string"}); - const data = { - crossValue: "tc-1", - mainValue: 101 - }; - - generateHtml(tooltip, data, settings); - expect(getContent()).toEqual(["cross-1: tc-1", "main-1: 101"]); - }); - - test("show with multiple crossValues", () => { - settings.crossValues.push({name: "cross-1", type: "string"}); - settings.crossValues.push({name: "cross-2", type: "integer"}); - const data = { - crossValue: "tc-1|1001", - mainValue: 101 - }; - - generateHtml(tooltip, data, settings); - expect(getContent()).toEqual(["cross-1: tc-1", "cross-2: 1,001", "main-1: 101"]); - }); - - test("show with single splitValue", () => { - settings.splitValues.push({name: "split-1", type: "string"}); - const data = { - key: "ts-1", - mainValue: 101 - }; - - generateHtml(tooltip, data, settings); - expect(getContent()).toEqual(["split-1: ts-1", "main-1: 101"]); - }); - - test("show with multiple splitValues", () => { - settings.splitValues.push({name: "split-1", type: "string"}); - settings.splitValues.push({name: "split-2", type: "integer"}); - const data = { - key: "ts-1|1001", - mainValue: 101 - }; - - generateHtml(tooltip, data, settings); - expect(getContent()).toEqual(["split-1: ts-1", "split-2: 1,001", "main-1: 101"]); - }); -}); +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ +import {select} from "d3"; +import {generateHtml} from "../../../../src/js/tooltip/generateHTML"; + +describe("tooltip generateHTML should", () => { + let tooltip = null; + let settings = null; + + beforeEach(() => { + tooltip = select("body") + .append("div") + .classed("tooltip-test", true); + tooltip.append("div").attr("id", "tooltip-values"); + + settings = { + crossValues: [], + splitValues: [], + mainValues: [{name: "main-1", type: "integer"}] + }; + }); + afterEach(() => { + tooltip.remove(); + }); + + const getContent = () => { + const content = []; + tooltip.selectAll("li").each((d, i, nodes) => { + content.push(select(nodes[i]).text()); + }); + return content; + }; + + test("show single mainValue", () => { + const data = { + mainValue: 101 + }; + generateHtml(tooltip, data, settings); + expect(getContent()).toEqual(["main-1: 101"]); + }); + + test("show multiple mainValues", () => { + settings.mainValues.push({name: "main-2", type: "float"}); + const data = { + mainValues: [101, 202.22] + }; + generateHtml(tooltip, data, settings); + expect(getContent()).toEqual(["main-1: 101", "main-2: 202.22"]); + }); + + test("format mainValue as date", () => { + settings.mainValues[0].type = "datetime"; + const testDate = new Date("2019-04-03T15:15Z"); + const data = { + mainValue: testDate.getTime() + }; + generateHtml(tooltip, data, settings); + expect(getContent()).toEqual([`main-1: ${testDate.toLocaleString()}`]); + }); + + test("format mainValue as integer", () => { + settings.mainValues[0].type = "integer"; + const data = { + mainValue: 12345.6789 + }; + generateHtml(tooltip, data, settings); + expect(getContent()).toEqual(["main-1: 12,345"]); + }); + + test("format mainValue as decimal", () => { + settings.mainValues[0].type = "float"; + const data = { + mainValue: 12345.6789 + }; + generateHtml(tooltip, data, settings); + expect(getContent()).toEqual(["main-1: 12,345.679"]); + }); + + test("show with single crossValue", () => { + settings.crossValues.push({name: "cross-1", type: "string"}); + const data = { + crossValue: "tc-1", + mainValue: 101 + }; + + generateHtml(tooltip, data, settings); + expect(getContent()).toEqual(["cross-1: tc-1", "main-1: 101"]); + }); + + test("show with multiple crossValues", () => { + settings.crossValues.push({name: "cross-1", type: "string"}); + settings.crossValues.push({name: "cross-2", type: "integer"}); + const data = { + crossValue: "tc-1|1001", + mainValue: 101 + }; + + generateHtml(tooltip, data, settings); + expect(getContent()).toEqual(["cross-1: tc-1", "cross-2: 1,001", "main-1: 101"]); + }); + + test("show with single splitValue", () => { + settings.splitValues.push({name: "split-1", type: "string"}); + const data = { + key: "ts-1", + mainValue: 101 + }; + + generateHtml(tooltip, data, settings); + expect(getContent()).toEqual(["split-1: ts-1", "main-1: 101"]); + }); + + test("show with multiple splitValues", () => { + settings.splitValues.push({name: "split-1", type: "string"}); + settings.splitValues.push({name: "split-2", type: "integer"}); + const data = { + key: "ts-1|1001", + mainValue: 101 + }; + + generateHtml(tooltip, data, settings); + expect(getContent()).toEqual(["split-1: ts-1", "split-2: 1,001", "main-1: 101"]); + }); +}); diff --git a/packages/perspective-viewer-d3fc/test/js/unit/tooltip/tooltip.spec.js b/packages/perspective-viewer-d3fc/test/js/unit/tooltip/tooltip.spec.js index 09ed64cb63..99aa6597b5 100644 --- a/packages/perspective-viewer-d3fc/test/js/unit/tooltip/tooltip.spec.js +++ b/packages/perspective-viewer-d3fc/test/js/unit/tooltip/tooltip.spec.js @@ -1,104 +1,104 @@ -/****************************************************************************** - * - * Copyright (c) 2017, the Perspective Authors. - * - * This file is part of the Perspective library, distributed under the terms of - * the Apache License 2.0. The full license can be found in the LICENSE file. - * - */ -import {select} from "d3"; -import {tooltip} from "../../../../src/js/tooltip/tooltip"; - -describe("tooltip with", () => { - let container = null; - let testElement = null; - let settings = null; - const data = [{mainValue: 101}]; - - const awaitTransition = selection => { - return new Promise(resolve => { - const transition = selection.transition(); - let n = transition.size(); - if (n === 0) { - resolve(); - } - transition.on("end", () => { - if (!--n) { - resolve(); - } - }); - }); - }; - - beforeEach(() => { - container = select("body") - .append("div") - .attr("id", "container") - .classed("chart", true); - - testElement = container - .selectAll("div.element") - .data(data) - .enter() - .append("div") - .classed("element", true) - .style("position", "absolute") - .style("top", "150px") - .style("left", "300px") - .style("width", "25px") - .style("height", "40px"); - - settings = { - crossValues: [], - splitValues: [], - mainValues: [{name: "main-1", type: "integer"}] - }; - }); - afterEach(() => { - container.remove(); - }); - - describe("on-hover should", () => { - let tooltipDiv; - beforeEach(async () => { - tooltip().settings(settings)(testElement); - tooltipDiv = container.select("div.tooltip"); - await awaitTransition(tooltipDiv); - }); - - test("not show a tooltip initially", () => { - expect(tooltipDiv.style("opacity")).toEqual("0"); - }); - - test("show a tooltip on mouse over", async () => { - testElement.node().dispatchEvent(new MouseEvent("mouseover")); - await awaitTransition(tooltipDiv); - - expect(tooltipDiv.style("opacity")).not.toEqual("0"); - }); - }); - - describe("always-show should", () => { - let tooltipDiv; - let tooltipComponent; - beforeEach(async () => { - tooltipComponent = tooltip() - .settings(settings) - .alwaysShow(true); - - tooltipComponent(testElement); - tooltipDiv = container.select("div.tooltip"); - await awaitTransition(tooltipDiv); - }); - - test("show a tooltip initially", () => { - expect(tooltipDiv.style("opacity")).not.toEqual("0"); - }); - - test("hide a tooltip if no element", async () => { - tooltipComponent(container.select("div.notexists")); - await awaitTransition(tooltipDiv); - expect(Math.floor(parseFloat(tooltipDiv.style("opacity")))).toEqual(0); - }); - }); -}); +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ +import {select} from "d3"; +import {tooltip} from "../../../../src/js/tooltip/tooltip"; + +describe("tooltip with", () => { + let container = null; + let testElement = null; + let settings = null; + const data = [{mainValue: 101}]; + + const awaitTransition = selection => { + return new Promise(resolve => { + const transition = selection.transition(); + let n = transition.size(); + if (n === 0) { + resolve(); + } + transition.on("end", () => { + if (!--n) { + resolve(); + } + }); + }); + }; + + beforeEach(() => { + container = select("body") + .append("div") + .attr("id", "container") + .classed("chart", true); + + testElement = container + .selectAll("div.element") + .data(data) + .enter() + .append("div") + .classed("element", true) + .style("position", "absolute") + .style("top", "150px") + .style("left", "300px") + .style("width", "25px") + .style("height", "40px"); + + settings = { + crossValues: [], + splitValues: [], + mainValues: [{name: "main-1", type: "integer"}] + }; + }); + afterEach(() => { + container.remove(); + }); + + describe("on-hover should", () => { + let tooltipDiv; + beforeEach(async () => { + tooltip().settings(settings)(testElement); + tooltipDiv = container.select("div.tooltip"); + await awaitTransition(tooltipDiv); + }); + + test("not show a tooltip initially", () => { + expect(tooltipDiv.style("opacity")).toEqual("0"); + }); + + test("show a tooltip on mouse over", async () => { + testElement.node().dispatchEvent(new MouseEvent("mouseover")); + await awaitTransition(tooltipDiv); + + expect(tooltipDiv.style("opacity")).not.toEqual("0"); + }); + }); + + describe("always-show should", () => { + let tooltipDiv; + let tooltipComponent; + beforeEach(async () => { + tooltipComponent = tooltip() + .settings(settings) + .alwaysShow(true); + + tooltipComponent(testElement); + tooltipDiv = container.select("div.tooltip"); + await awaitTransition(tooltipDiv); + }); + + test("show a tooltip initially", () => { + expect(tooltipDiv.style("opacity")).not.toEqual("0"); + }); + + test("hide a tooltip if no element", async () => { + tooltipComponent(container.select("div.notexists")); + await awaitTransition(tooltipDiv); + expect(Math.floor(parseFloat(tooltipDiv.style("opacity")))).toEqual(0); + }); + }); +}); diff --git a/packages/perspective-viewer-datagrid/src/js/index.js b/packages/perspective-viewer-datagrid/src/js/index.js index ba30ec277f..2ff6579ba7 100644 --- a/packages/perspective-viewer-datagrid/src/js/index.js +++ b/packages/perspective-viewer-datagrid/src/js/index.js @@ -1,125 +1,125 @@ -/****************************************************************************** - * - * Copyright (c) 2017, the Perspective Authors. - * - * This file is part of the Perspective library, distributed under the terms of - * the Apache License 2.0. The full license can be found in the LICENSE file. - * - */ - -import {registerPlugin} from "@finos/perspective-viewer/dist/esm/utils.js"; - -import "regular-table"; -import MATERIAL_STYLE from "regular-table/dist/less/material.less"; - -const VIEWER_MAP = new WeakMap(); - -/** - * Initializes a new datagrid renderer if needed, or returns an existing one - * associated with a rendering `