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 `
` from a cache. - * - * @param {*} element - * @param {*} div - * @returns - */ -function get_or_create_datagrid(element, div) { - let datagrid; - if (!VIEWER_MAP.has(div)) { - datagrid = document.createElement("regular-table"); - div.innerHTML = ""; - div.appendChild(document.createElement("slot")); - element.appendChild(datagrid); - VIEWER_MAP.set(div, datagrid); - } else { - datagrid = VIEWER_MAP.get(div); - if (!datagrid.isConnected) { - datagrid.clear(); - div.innerHTML = ""; - div.appendChild(document.createElement("slot")); - element.appendChild(datagrid); - } - } - - return datagrid; -} - -/** - * plugin. - * - * @class DatagridPlugin - */ -class DatagridPlugin { - static name = "Datagrid"; - static selectMode = "toggle"; - static deselectMode = "pivots"; - - static async update(div) { - try { - const datagrid = VIEWER_MAP.get(div); - await datagrid.draw({invalid_viewport: true}); - } catch (e) { - return; - } - } - - static async create(div, view) { - const datagrid = get_or_create_datagrid(this, div); - const options = await datagrid.set_view(this.table, view); - if (this._plugin_config) { - datagrid.restore(this._plugin_config); - delete this._plugin_config; - } - await datagrid.draw(options); - } - - static async resize() { - if (this.view && VIEWER_MAP.has(this._datavis)) { - const datagrid = VIEWER_MAP.get(this._datavis); - await datagrid.draw({invalid_viewport: true}); - } - } - - static delete() { - if (this.view && VIEWER_MAP.has(this._datavis)) { - const datagrid = VIEWER_MAP.get(this._datavis); - datagrid.clear(); - } - } - - static save() { - if (this.view && VIEWER_MAP.has(this._datavis)) { - const datagrid = VIEWER_MAP.get(this._datavis); - return datagrid.save(); - } - } - - static restore(config) { - if (this.view && VIEWER_MAP.has(this._datavis)) { - const datagrid = VIEWER_MAP.get(this._datavis); - datagrid.restore(config); - } else { - this._plugin_config = config; - } - } -} - -/** - * Appends the default tbale CSS to ``, should be run once on module - * import. - * - */ -function _register_global_styles() { - const style = document.createElement("style"); - style.textContent = MATERIAL_STYLE; - document.head.appendChild(style); -} - -/****************************************************************************** - * - * Main - * - */ - -registerPlugin("datagrid", DatagridPlugin); - -_register_global_styles(); +/****************************************************************************** + * + * 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 `
` from a cache. + * + * @param {*} element + * @param {*} div + * @returns + */ +function get_or_create_datagrid(element, div) { + let datagrid; + if (!VIEWER_MAP.has(div)) { + datagrid = document.createElement("regular-table"); + div.innerHTML = ""; + div.appendChild(document.createElement("slot")); + element.appendChild(datagrid); + VIEWER_MAP.set(div, datagrid); + } else { + datagrid = VIEWER_MAP.get(div); + if (!datagrid.isConnected) { + datagrid.clear(); + div.innerHTML = ""; + div.appendChild(document.createElement("slot")); + element.appendChild(datagrid); + } + } + + return datagrid; +} + +/** + * plugin. + * + * @class DatagridPlugin + */ +class DatagridPlugin { + static name = "Datagrid"; + static selectMode = "toggle"; + static deselectMode = "pivots"; + + static async update(div) { + try { + const datagrid = VIEWER_MAP.get(div); + await datagrid.draw({invalid_viewport: true}); + } catch (e) { + return; + } + } + + static async create(div, view) { + const datagrid = get_or_create_datagrid(this, div); + const options = await datagrid.set_view(this.table, view); + if (this._plugin_config) { + datagrid.restore(this._plugin_config); + delete this._plugin_config; + } + await datagrid.draw(options); + } + + static async resize() { + if (this.view && VIEWER_MAP.has(this._datavis)) { + const datagrid = VIEWER_MAP.get(this._datavis); + await datagrid.draw({invalid_viewport: true}); + } + } + + static delete() { + if (this.view && VIEWER_MAP.has(this._datavis)) { + const datagrid = VIEWER_MAP.get(this._datavis); + datagrid.clear(); + } + } + + static save() { + if (this.view && VIEWER_MAP.has(this._datavis)) { + const datagrid = VIEWER_MAP.get(this._datavis); + return datagrid.save(); + } + } + + static restore(config) { + if (this.view && VIEWER_MAP.has(this._datavis)) { + const datagrid = VIEWER_MAP.get(this._datavis); + datagrid.restore(config); + } else { + this._plugin_config = config; + } + } +} + +/** + * Appends the default tbale CSS to ``, should be run once on module + * import. + * + */ +function _register_global_styles() { + const style = document.createElement("style"); + style.textContent = MATERIAL_STYLE; + document.head.appendChild(style); +} + +/****************************************************************************** + * + * Main + * + */ + +registerPlugin("datagrid", DatagridPlugin); + +_register_global_styles(); diff --git a/packages/perspective-viewer-highcharts/src/js/highcharts/config.js b/packages/perspective-viewer-highcharts/src/js/highcharts/config.js index 6859c6bc54..b315dbda0b 100644 --- a/packages/perspective-viewer-highcharts/src/js/highcharts/config.js +++ b/packages/perspective-viewer-highcharts/src/js/highcharts/config.js @@ -1,325 +1,325 @@ -/****************************************************************************** - * - * 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 tooltip from "./tooltip"; - -export function set_boost(config, series, ...types) { - const count = config.series[0].data ? config.series[0].data.length * config.series.length : config.series.length; - if (count > 5000) { - Object.assign(config, { - boost: { - useGPUTranslations: types.indexOf("datetime") === -1 && types.indexOf("date") === -1, - usePreAllocated: types.indexOf("datetime") === -1 && types.indexOf("date") === -1 - } - }); - config.plotOptions.series.boostThreshold = 1; - config.plotOptions.series.turboThreshold = 0; - return true; - } -} - -export function set_tick_size(config) { - let new_radius = Math.min(6, Math.max(3, Math.floor((this.clientWidth + this.clientHeight) / Math.max(300, config.series[0].data.length / 3)))); - config.plotOptions.coloredScatter = {marker: {radius: new_radius}}; - config.plotOptions.scatter = {marker: {radius: new_radius}}; -} - -export function set_both_axis(config, axis, name, type, tree_type, top) { - if (type === "string") { - set_category_axis(config, axis, tree_type, top); - } else { - set_axis(config, axis, name, type); - } -} - -export function set_axis(config, axis, name, type) { - let opts = { - type: ["datetime", "date"].indexOf(type) > -1 ? "datetime" : undefined, - startOnTick: false, - endOnTick: false, - title: { - style: {color: "#666666", fontSize: "14px"}, - text: name - } - }; - if (axis === "yAxis") { - Object.assign(opts, {labels: {overflow: "justify"}}); - } - Object.assign(config, {[axis]: opts}); -} - -export function set_category_axis(config, axis, type, top) { - if (type === "datetime") { - Object.assign(config, { - [axis]: { - categories: top.categories.map(x => new Date(x).toLocaleString("en-us", {year: "numeric", month: "numeric", day: "numeric", hour: "numeric", minute: "numeric"})), - labels: { - enabled: top.categories.length > 0, - autoRotation: [-5] - } - } - }); - } else if (type === "date") { - Object.assign(config, { - [axis]: { - categories: top.categories.map(x => new Date(x).toLocaleString("en-us", {year: "numeric", month: "numeric", day: "numeric"})), - labels: { - enabled: top.categories.length > 0, - autoRotation: [-5] - } - } - }); - } else { - let opts = { - categories: top.categories, - labels: { - enabled: top.categories.length > 0, - padding: 0, - autoRotation: [-10, -20, -30, -40, -50, -60, -70, -80, -90] - } - }; - if (axis === "yAxis" && (!config.hasOwnProperty("boost") || config.chart.type === "heatmap")) { - Object.assign(opts, { - title: null, - tickWidth: 1, - reversed: true - }); - } - Object.assign(config, {[axis]: opts}); - } -} - -export function default_config(aggregates, mode) { - let type = "scatter"; - let hover_type = "xy"; - if (mode === "y_line") { - hover_type = "y"; - type = "line"; - } else if (mode === "y_area") { - hover_type = "y"; - type = "area"; - } else if (mode === "y_scatter") { - hover_type = "y"; - type = "scatter"; - } else if (mode.indexOf("bar") > -1) { - hover_type = "y"; - type = "column"; - } else if (mode == "treemap") { - hover_type = "hierarchy"; - type = "treemap"; - } else if (mode == "sunburst") { - hover_type = "hierarchy"; - type = "sunburst"; - } else if (mode === "scatter") { - hover_type = "xy"; - if (aggregates.length <= 3 || aggregates[3] === null) { - type = "scatter"; - } else { - type = "bubble"; - } - } else if (mode === "heatmap") { - hover_type = "xyz"; - type = "heatmap"; - } - - /* eslint-disable max-len */ - - // let new_radius = 0; - // if (mode === 'scatter') { - // new_radius = Math.min(8, Math.max(4, Math.floor((this.clientWidth + this.clientHeight) / Math.max(300, series[0].data.length / 3)))); - // } - // - - // read this + define chart schema using _view() - - /* eslint-enable max-len */ - - const that = this; - const config = this._config; - const pivot_titles = get_pivot_titles(config.row_pivots, config.column_pivots); - - const is_empty = str => str.replace(/\s/g, "") == ""; - - return { - chart: { - type: type, - inverted: mode.slice(0, 2) === "x_", - animation: false, - zoomType: mode === "scatter" ? "xy" : "x", - resetZoomButton: { - position: { - align: "left" - } - } - }, - navigation: { - buttonOptions: { - enabled: false - } - }, - credits: {enabled: false}, - title: { - text: null - }, - legend: { - align: "right", - verticalAlign: "top", - y: 10, - layout: "vertical", - enabled: false, - itemStyle: { - fontWeight: "normal" - } - }, - boost: { - enabled: false - }, - - plotOptions: { - area: { - stacking: "normal", - marker: {enabled: false, radius: 0} - }, - line: { - marker: {enabled: false, radius: 0} - }, - coloredScatter: { - // marker: {radius: new_radius}, - }, - scatter: { - // marker: {radius: new_radius}, - }, - column: { - stacking: "normal", - states: { - hover: { - // add ajax - brightness: -0.1, - borderColor: "#000000" - } - } - }, - heatmap: { - nullColor: "rgba(0,0,0,0)" - }, - series: { - animation: false, - nullColor: "rgba(0,0,0,0)", - boostThreshold: 0, - turboThreshold: 60000, - borderWidth: 0, - connectNulls: true, - lineWidth: mode.indexOf("line") === -1 ? 0 : 1.5, - states: { - hover: { - lineWidthPlus: 0 - } - }, - point: { - events: { - click: async function() { - let row_pivots_values = []; - let column_pivot_values = []; - if ((type === "bubble" && mode === "scatter") || (type === "scatter" && mode === "scatter") || (type === "scatter" && mode === "line")) { - column_pivot_values = this.series.userOptions.name.split(", "); - row_pivots_values = this.name ? this.name.split(", ") : []; - } else if (type === "column" || type === "line" || type === "scatter" || type === "area") { - column_pivot_values = this.series.userOptions.name.split(", "); - row_pivots_values = tooltip.get_pivot_values(this.category); - } else { - console.log(`Click dispatch for ${mode} ${type} not supported.`); - return; - } - - const row_filters = config.row_pivots.map((c, index) => [c, "==", row_pivots_values[index]]); - const column_filters = config.column_pivots.map((c, index) => [c, "==", column_pivot_values[index]]); - const filters = config.filter - .concat(row_filters) - .concat(column_filters) - .filter(f => !!f[f.length - 1]); - - const start_row = this.index + 1; - const end_row = start_row + 1; - let column_names = []; - if ((type === "bubble" && mode === "scatter") || (type === "scatter" && mode === "scatter") || (type === "scatter" && mode === "line")) { - column_names = aggregates; - } else { - const stack_name = this.series.userOptions ? this.series.userOptions.stack : ""; - const column_name = column_pivot_values[column_pivot_values.length - 1]; - if (is_empty(column_name)) column_names.push(stack_name); - else column_names.push(column_name); - } - - const r = await that._view.to_json({start_row, end_row}); - that.dispatchEvent( - new CustomEvent("perspective-click", { - bubbles: true, - composed: true, - detail: { - column_names, - config: {filters}, - row: r[0] - } - }) - ); - } - } - } - } - }, - tooltip: { - animation: false, - backgroundColor: "#FFFFFF", - borderColor: "#777777", - followPointer: false, - valueDecimals: 2, - formatter: function(highcharts_tooltip) { - that._view - .schema(false) - .then(schema => { - let tooltip_text = tooltip.format_tooltip(this, hover_type, schema, aggregates, pivot_titles); - highcharts_tooltip.label.attr({ - text: tooltip_text - }); - }) - .catch(err => console.error(err)); - - return "Loading..."; - }, - positioner: function(labelWidth, labelHeight, point) { - let chart = this.chart; - let tooltipX, tooltipY; - - if (point.plotX + labelWidth > chart.plotWidth) { - tooltipX = point.plotX + chart.plotLeft - labelWidth - 5; - } else { - tooltipX = point.plotX + chart.plotLeft; - } - - if (point.plotY + labelHeight > chart.plotHeight) { - tooltipY = point.plotY + chart.plotTop - labelHeight; - } else { - tooltipY = point.plotY + chart.plotTop; - } - - return { - x: tooltipX, - y: tooltipY - }; - } - } - }; -} - -function get_pivot_titles(row_pivots, column_pivots) { - return { - row: row_pivots, - column: column_pivots - }; -} +/****************************************************************************** + * + * 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 tooltip from "./tooltip"; + +export function set_boost(config, series, ...types) { + const count = config.series[0].data ? config.series[0].data.length * config.series.length : config.series.length; + if (count > 5000) { + Object.assign(config, { + boost: { + useGPUTranslations: types.indexOf("datetime") === -1 && types.indexOf("date") === -1, + usePreAllocated: types.indexOf("datetime") === -1 && types.indexOf("date") === -1 + } + }); + config.plotOptions.series.boostThreshold = 1; + config.plotOptions.series.turboThreshold = 0; + return true; + } +} + +export function set_tick_size(config) { + let new_radius = Math.min(6, Math.max(3, Math.floor((this.clientWidth + this.clientHeight) / Math.max(300, config.series[0].data.length / 3)))); + config.plotOptions.coloredScatter = {marker: {radius: new_radius}}; + config.plotOptions.scatter = {marker: {radius: new_radius}}; +} + +export function set_both_axis(config, axis, name, type, tree_type, top) { + if (type === "string") { + set_category_axis(config, axis, tree_type, top); + } else { + set_axis(config, axis, name, type); + } +} + +export function set_axis(config, axis, name, type) { + let opts = { + type: ["datetime", "date"].indexOf(type) > -1 ? "datetime" : undefined, + startOnTick: false, + endOnTick: false, + title: { + style: {color: "#666666", fontSize: "14px"}, + text: name + } + }; + if (axis === "yAxis") { + Object.assign(opts, {labels: {overflow: "justify"}}); + } + Object.assign(config, {[axis]: opts}); +} + +export function set_category_axis(config, axis, type, top) { + if (type === "datetime") { + Object.assign(config, { + [axis]: { + categories: top.categories.map(x => new Date(x).toLocaleString("en-us", {year: "numeric", month: "numeric", day: "numeric", hour: "numeric", minute: "numeric"})), + labels: { + enabled: top.categories.length > 0, + autoRotation: [-5] + } + } + }); + } else if (type === "date") { + Object.assign(config, { + [axis]: { + categories: top.categories.map(x => new Date(x).toLocaleString("en-us", {year: "numeric", month: "numeric", day: "numeric"})), + labels: { + enabled: top.categories.length > 0, + autoRotation: [-5] + } + } + }); + } else { + let opts = { + categories: top.categories, + labels: { + enabled: top.categories.length > 0, + padding: 0, + autoRotation: [-10, -20, -30, -40, -50, -60, -70, -80, -90] + } + }; + if (axis === "yAxis" && (!config.hasOwnProperty("boost") || config.chart.type === "heatmap")) { + Object.assign(opts, { + title: null, + tickWidth: 1, + reversed: true + }); + } + Object.assign(config, {[axis]: opts}); + } +} + +export function default_config(aggregates, mode) { + let type = "scatter"; + let hover_type = "xy"; + if (mode === "y_line") { + hover_type = "y"; + type = "line"; + } else if (mode === "y_area") { + hover_type = "y"; + type = "area"; + } else if (mode === "y_scatter") { + hover_type = "y"; + type = "scatter"; + } else if (mode.indexOf("bar") > -1) { + hover_type = "y"; + type = "column"; + } else if (mode == "treemap") { + hover_type = "hierarchy"; + type = "treemap"; + } else if (mode == "sunburst") { + hover_type = "hierarchy"; + type = "sunburst"; + } else if (mode === "scatter") { + hover_type = "xy"; + if (aggregates.length <= 3 || aggregates[3] === null) { + type = "scatter"; + } else { + type = "bubble"; + } + } else if (mode === "heatmap") { + hover_type = "xyz"; + type = "heatmap"; + } + + /* eslint-disable max-len */ + + // let new_radius = 0; + // if (mode === 'scatter') { + // new_radius = Math.min(8, Math.max(4, Math.floor((this.clientWidth + this.clientHeight) / Math.max(300, series[0].data.length / 3)))); + // } + // + + // read this + define chart schema using _view() + + /* eslint-enable max-len */ + + const that = this; + const config = this._config; + const pivot_titles = get_pivot_titles(config.row_pivots, config.column_pivots); + + const is_empty = str => str.replace(/\s/g, "") == ""; + + return { + chart: { + type: type, + inverted: mode.slice(0, 2) === "x_", + animation: false, + zoomType: mode === "scatter" ? "xy" : "x", + resetZoomButton: { + position: { + align: "left" + } + } + }, + navigation: { + buttonOptions: { + enabled: false + } + }, + credits: {enabled: false}, + title: { + text: null + }, + legend: { + align: "right", + verticalAlign: "top", + y: 10, + layout: "vertical", + enabled: false, + itemStyle: { + fontWeight: "normal" + } + }, + boost: { + enabled: false + }, + + plotOptions: { + area: { + stacking: "normal", + marker: {enabled: false, radius: 0} + }, + line: { + marker: {enabled: false, radius: 0} + }, + coloredScatter: { + // marker: {radius: new_radius}, + }, + scatter: { + // marker: {radius: new_radius}, + }, + column: { + stacking: "normal", + states: { + hover: { + // add ajax + brightness: -0.1, + borderColor: "#000000" + } + } + }, + heatmap: { + nullColor: "rgba(0,0,0,0)" + }, + series: { + animation: false, + nullColor: "rgba(0,0,0,0)", + boostThreshold: 0, + turboThreshold: 60000, + borderWidth: 0, + connectNulls: true, + lineWidth: mode.indexOf("line") === -1 ? 0 : 1.5, + states: { + hover: { + lineWidthPlus: 0 + } + }, + point: { + events: { + click: async function() { + let row_pivots_values = []; + let column_pivot_values = []; + if ((type === "bubble" && mode === "scatter") || (type === "scatter" && mode === "scatter") || (type === "scatter" && mode === "line")) { + column_pivot_values = this.series.userOptions.name.split(", "); + row_pivots_values = this.name ? this.name.split(", ") : []; + } else if (type === "column" || type === "line" || type === "scatter" || type === "area") { + column_pivot_values = this.series.userOptions.name.split(", "); + row_pivots_values = tooltip.get_pivot_values(this.category); + } else { + console.log(`Click dispatch for ${mode} ${type} not supported.`); + return; + } + + const row_filters = config.row_pivots.map((c, index) => [c, "==", row_pivots_values[index]]); + const column_filters = config.column_pivots.map((c, index) => [c, "==", column_pivot_values[index]]); + const filters = config.filter + .concat(row_filters) + .concat(column_filters) + .filter(f => !!f[f.length - 1]); + + const start_row = this.index + 1; + const end_row = start_row + 1; + let column_names = []; + if ((type === "bubble" && mode === "scatter") || (type === "scatter" && mode === "scatter") || (type === "scatter" && mode === "line")) { + column_names = aggregates; + } else { + const stack_name = this.series.userOptions ? this.series.userOptions.stack : ""; + const column_name = column_pivot_values[column_pivot_values.length - 1]; + if (is_empty(column_name)) column_names.push(stack_name); + else column_names.push(column_name); + } + + const r = await that._view.to_json({start_row, end_row}); + that.dispatchEvent( + new CustomEvent("perspective-click", { + bubbles: true, + composed: true, + detail: { + column_names, + config: {filters}, + row: r[0] + } + }) + ); + } + } + } + } + }, + tooltip: { + animation: false, + backgroundColor: "#FFFFFF", + borderColor: "#777777", + followPointer: false, + valueDecimals: 2, + formatter: function(highcharts_tooltip) { + that._view + .schema(false) + .then(schema => { + let tooltip_text = tooltip.format_tooltip(this, hover_type, schema, aggregates, pivot_titles); + highcharts_tooltip.label.attr({ + text: tooltip_text + }); + }) + .catch(err => console.error(err)); + + return "Loading..."; + }, + positioner: function(labelWidth, labelHeight, point) { + let chart = this.chart; + let tooltipX, tooltipY; + + if (point.plotX + labelWidth > chart.plotWidth) { + tooltipX = point.plotX + chart.plotLeft - labelWidth - 5; + } else { + tooltipX = point.plotX + chart.plotLeft; + } + + if (point.plotY + labelHeight > chart.plotHeight) { + tooltipY = point.plotY + chart.plotTop - labelHeight; + } else { + tooltipY = point.plotY + chart.plotTop; + } + + return { + x: tooltipX, + y: tooltipY + }; + } + } + }; +} + +function get_pivot_titles(row_pivots, column_pivots) { + return { + row: row_pivots, + column: column_pivots + }; +} diff --git a/packages/perspective-viewer-highcharts/src/js/highcharts/draw.js b/packages/perspective-viewer-highcharts/src/js/highcharts/draw.js index e20e1a40b3..2eaa4e2edc 100644 --- a/packages/perspective-viewer-highcharts/src/js/highcharts/draw.js +++ b/packages/perspective-viewer-highcharts/src/js/highcharts/draw.js @@ -1,322 +1,322 @@ -/****************************************************************************** - * - * 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 Highcharts from "highcharts"; - -import style from "../../less/highcharts.less"; -import template from "../../html/highcharts.html"; - -import {COLORS_10, COLORS_20} from "./externals.js"; -import {color_axis} from "./color_axis.js"; -import {make_tree_data, make_y_data, make_xy_data, make_xyz_data, make_xy_column_data} from "./series.js"; -import {set_boost, set_category_axis, set_both_axis, default_config, set_tick_size} from "./config.js"; -import {bindTemplate} from "@finos/perspective-viewer/dist/esm/utils"; -import detectIE from "detectie"; - -export const PRIVATE = Symbol("Highcharts private"); - -function get_or_create_element(div) { - let perspective_highcharts_element; - this[PRIVATE] = this[PRIVATE] || {}; - if (!this[PRIVATE].chart) { - perspective_highcharts_element = this[PRIVATE].chart = document.createElement("perspective-highcharts"); - } else { - perspective_highcharts_element = this[PRIVATE].chart; - } - - if (!document.body.contains(perspective_highcharts_element)) { - div.innerHTML = ""; - div.appendChild(perspective_highcharts_element); - } - return perspective_highcharts_element; -} - -export const draw = (mode, set_config, restyle) => - async function(el, view, task, end_col, end_row) { - if (set_config) { - this._config = await view.get_config(); - if (task.cancelled) { - return; - } - } - - const config = await view.get_config(); - - const row_pivots = config.row_pivots; - const col_pivots = config.column_pivots; - const columns = config.columns; - const real_columns = JSON.parse(this.getAttribute("columns")); - - const [schema, tschema] = await Promise.all([view.schema(false), this._table.schema(false)]); - let element; - - if (task.cancelled) { - return; - } - - let configs = [], - xaxis_name = columns.length > 0 ? columns[0] : undefined, - xaxis_type = schema[xaxis_name], - yaxis_name = columns.length > 1 ? columns[1] : undefined, - yaxis_type = schema[yaxis_name], - xtree_name = row_pivots.length > 0 ? row_pivots[row_pivots.length - 1] : undefined, - xtree_type = tschema[xtree_name], - ytree_name = col_pivots.length > 0 ? col_pivots[col_pivots.length - 1] : undefined, - ytree_type = tschema[ytree_name], - num_aggregates = columns.length; - - try { - if (mode === "scatter") { - let cols; - if (end_col || end_row) { - cols = await view.to_columns({end_col, end_row, leaves_only: true}); - } else { - cols = await view.to_columns(); - } - const config = (configs[0] = default_config.call(this, real_columns, mode)); - const [series, xtop, colorRange, ytop] = make_xy_column_data(cols, schema, real_columns, row_pivots, col_pivots); - - config.legend.floating = series.length <= 20; - config.legend.enabled = col_pivots.length > 0; - config.series = series; - config.colors = series.length <= 10 ? COLORS_10 : COLORS_20; - if (colorRange[0] !== Infinity) { - if (real_columns.length <= 3 || real_columns[3] === null) { - config.chart.type = "coloredScatter"; - } else { - config.chart.type = "coloredBubble"; - } - color_axis.call(this, config, colorRange, restyle); - } else if (real_columns.length > 3 && real_columns[3] !== null) { - config.chart.type = "bubble"; - } - - if (num_aggregates < 3) { - set_boost(config, xaxis_type, yaxis_type); - } - set_both_axis(config, "xAxis", xaxis_name, xaxis_type, xaxis_type, xtop); - set_both_axis(config, "yAxis", yaxis_name, yaxis_type, yaxis_type, ytop); - set_tick_size.call(this, config); - } else if (mode === "heatmap") { - let js; - if (end_col || end_row) { - js = await view.to_json({end_col, end_row, leaves_only: false}); - } else { - js = await view.to_json(); - } - let config = (configs[0] = default_config.call(this, columns, mode)); - let [series, top, ytop, colorRange] = make_xyz_data(js, row_pivots, ytree_type); - config.series = [ - { - name: null, - data: series, - nullColor: "none" - } - ]; - config.legend.enabled = true; - config.legend.floating = false; - - color_axis.call(this, config, colorRange, restyle); - set_boost(config, xaxis_type, yaxis_type); - set_category_axis(config, "xAxis", xtree_type, top); - set_category_axis(config, "yAxis", ytree_type, ytop); - } else if (mode === "treemap" || mode === "sunburst") { - let js; - if (end_col || end_row) { - js = await view.to_json({end_col, end_row, leaves_only: false}); - } else { - js = await view.to_json(); - } - let [charts, , colorRange] = make_tree_data(js, row_pivots, columns, mode === "treemap"); - for (let series of charts) { - let config = default_config.call(this, columns, mode); - config.series = [series]; - if (charts.length > 1) { - config.title.text = series.title; - } - config.plotOptions.series.borderWidth = 1; - config.legend.floating = false; - if (colorRange) { - color_axis.call(this, config, colorRange, restyle); - } - configs.push(config); - } - } else if (mode === "line") { - let s; - let config = (configs[0] = default_config.call(this, columns, mode)); - - if (col_pivots.length === 0) { - let cols; - if (end_col || end_row) { - cols = await view.to_columns({end_col, end_row, leaves_only: true}); - } else { - cols = await view.to_columns(); - } - s = await make_xy_column_data(cols, schema, columns, row_pivots, col_pivots); - } else { - let js; - if (end_col || end_row) { - js = await view.to_json({end_col, end_row, leaves_only: false}); - } else { - js = await view.to_json(); - } - s = await make_xy_data(js, schema, columns, row_pivots, col_pivots); - } - - const series = s[0]; - const xtop = s[1]; - const ytop = s[3]; - - const colors = series.length <= 10 ? COLORS_10 : COLORS_20; - config.legend.floating = series.length <= 20; - config.legend.enabled = col_pivots.length > 0; - config.series = series; - config.plotOptions.scatter.marker = {enabled: false, radius: 0}; - config.colors = colors; - if (set_boost(config, xaxis_type, yaxis_type)) { - delete config.chart["type"]; - } - set_both_axis(config, "xAxis", xaxis_name, xaxis_type, xaxis_type, xtop); - set_both_axis(config, "yAxis", yaxis_name, yaxis_type, yaxis_type, ytop); - } else { - let config = (configs[0] = default_config.call(this, columns, mode)); - let cols; - if (end_col || end_row) { - cols = await view.to_columns({end_col, end_row, leaves_only: false}); - } else { - cols = await view.to_columns(); - } - - let [series, top] = make_y_data(cols, row_pivots); - config.series = series; - config.colors = series.length <= 10 ? COLORS_10 : COLORS_20; - config.legend.enabled = col_pivots.length > 0 || series.length > 1; - config.legend.floating = series.length <= 20; - config.plotOptions.series.dataLabels = { - allowOverlap: false, - padding: 10 - }; - if (mode.indexOf("scatter") > -1 || mode.indexOf("line") > -1) { - set_boost(config, xaxis_type, yaxis_type); - } - set_category_axis(config, "xAxis", xtree_type, top); - Object.assign(config, { - yAxis: { - startOnTick: false, - endOnTick: false, - title: { - text: columns.join(", "), - style: {color: "#666666", fontSize: "14px"} - }, - labels: {overflow: "justify"} - } - }); - } - } finally { - element = get_or_create_element.call(this, el); - if (restyle || this.hasAttribute("updating")) { - element.delete(); - } - } - - element.render(mode, configs, this); - }; - -@bindTemplate(template, style) // eslint-disable-next-line no-unused-vars -class HighchartsElement extends HTMLElement { - constructor() { - super(); - this._charts = []; - } - - connectedCallback() { - this._container = this.shadowRoot.querySelector("#container"); - } - - render(mode, configs, callee) { - if (this._charts.length > 0 && this._charts.length === configs.length) { - let idx = 0; - for (let cidx = 0; cidx < this._charts.length; cidx++) { - const chart = this._charts[cidx]; - let config = configs[idx++]; - if (config.boost) { - let target = chart.renderTo; - try { - chart.destroy(); - } catch (e) { - console.warn("Scatter plot destroy() call failed - this is probably leaking memory"); - } - this._charts[cidx] = Highcharts.chart(target, config); - } else if (mode === "scatter") { - let conf = { - series: config.series, - plotOptions: {} - }; - set_tick_size.call(callee, conf); - chart.update(conf); - } else { - let opts = {series: config.series, xAxis: config.xAxis, yAxis: config.yAxis}; - chart.update(opts); - } - } - } else { - this.delete(); - for (let config of configs) { - let chart = document.createElement("div"); - chart.className = "chart"; - this._container.appendChild(chart); - this._charts.push(() => Highcharts.chart(chart, config)); - } - - for (let i = 0; i < this._charts.length; i++) { - this._charts[i] = this._charts[i](); - } - } - - if (!this._charts.every(x => this._container.contains(x.renderTo))) { - this.remove(); - this._charts.map(x => this._container.appendChild(x.renderTo)); - } - - // TODO resize bug in Highcharts? - if (configs.length > 1) { - this.resize(); - } - - if (detectIE()) { - setTimeout(() => this.resize()); - } - } - - resize() { - if (this._charts && this._charts.length > 0) { - this._charts.map(x => x.reflow()); - } - } - - remove() { - this._charts = []; - for (let e of Array.prototype.slice.call(this._container.children)) { - if (e.tagName === "DIV") { - this._container.removeChild(e); - } - } - } - - delete() { - for (let chart of this._charts) { - try { - chart.destroy(); - } catch (e) { - console.warn("Scatter plot destroy() call failed - this is probably leaking memory"); - } - } - this.remove(); - } -} +/****************************************************************************** + * + * 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 Highcharts from "highcharts"; + +import style from "../../less/highcharts.less"; +import template from "../../html/highcharts.html"; + +import {COLORS_10, COLORS_20} from "./externals.js"; +import {color_axis} from "./color_axis.js"; +import {make_tree_data, make_y_data, make_xy_data, make_xyz_data, make_xy_column_data} from "./series.js"; +import {set_boost, set_category_axis, set_both_axis, default_config, set_tick_size} from "./config.js"; +import {bindTemplate} from "@finos/perspective-viewer/dist/esm/utils"; +import detectIE from "detectie"; + +export const PRIVATE = Symbol("Highcharts private"); + +function get_or_create_element(div) { + let perspective_highcharts_element; + this[PRIVATE] = this[PRIVATE] || {}; + if (!this[PRIVATE].chart) { + perspective_highcharts_element = this[PRIVATE].chart = document.createElement("perspective-highcharts"); + } else { + perspective_highcharts_element = this[PRIVATE].chart; + } + + if (!document.body.contains(perspective_highcharts_element)) { + div.innerHTML = ""; + div.appendChild(perspective_highcharts_element); + } + return perspective_highcharts_element; +} + +export const draw = (mode, set_config, restyle) => + async function(el, view, task, end_col, end_row) { + if (set_config) { + this._config = await view.get_config(); + if (task.cancelled) { + return; + } + } + + const config = await view.get_config(); + + const row_pivots = config.row_pivots; + const col_pivots = config.column_pivots; + const columns = config.columns; + const real_columns = JSON.parse(this.getAttribute("columns")); + + const [schema, tschema] = await Promise.all([view.schema(false), this._table.schema(false)]); + let element; + + if (task.cancelled) { + return; + } + + let configs = [], + xaxis_name = columns.length > 0 ? columns[0] : undefined, + xaxis_type = schema[xaxis_name], + yaxis_name = columns.length > 1 ? columns[1] : undefined, + yaxis_type = schema[yaxis_name], + xtree_name = row_pivots.length > 0 ? row_pivots[row_pivots.length - 1] : undefined, + xtree_type = tschema[xtree_name], + ytree_name = col_pivots.length > 0 ? col_pivots[col_pivots.length - 1] : undefined, + ytree_type = tschema[ytree_name], + num_aggregates = columns.length; + + try { + if (mode === "scatter") { + let cols; + if (end_col || end_row) { + cols = await view.to_columns({end_col, end_row, leaves_only: true}); + } else { + cols = await view.to_columns(); + } + const config = (configs[0] = default_config.call(this, real_columns, mode)); + const [series, xtop, colorRange, ytop] = make_xy_column_data(cols, schema, real_columns, row_pivots, col_pivots); + + config.legend.floating = series.length <= 20; + config.legend.enabled = col_pivots.length > 0; + config.series = series; + config.colors = series.length <= 10 ? COLORS_10 : COLORS_20; + if (colorRange[0] !== Infinity) { + if (real_columns.length <= 3 || real_columns[3] === null) { + config.chart.type = "coloredScatter"; + } else { + config.chart.type = "coloredBubble"; + } + color_axis.call(this, config, colorRange, restyle); + } else if (real_columns.length > 3 && real_columns[3] !== null) { + config.chart.type = "bubble"; + } + + if (num_aggregates < 3) { + set_boost(config, xaxis_type, yaxis_type); + } + set_both_axis(config, "xAxis", xaxis_name, xaxis_type, xaxis_type, xtop); + set_both_axis(config, "yAxis", yaxis_name, yaxis_type, yaxis_type, ytop); + set_tick_size.call(this, config); + } else if (mode === "heatmap") { + let js; + if (end_col || end_row) { + js = await view.to_json({end_col, end_row, leaves_only: false}); + } else { + js = await view.to_json(); + } + let config = (configs[0] = default_config.call(this, columns, mode)); + let [series, top, ytop, colorRange] = make_xyz_data(js, row_pivots, ytree_type); + config.series = [ + { + name: null, + data: series, + nullColor: "none" + } + ]; + config.legend.enabled = true; + config.legend.floating = false; + + color_axis.call(this, config, colorRange, restyle); + set_boost(config, xaxis_type, yaxis_type); + set_category_axis(config, "xAxis", xtree_type, top); + set_category_axis(config, "yAxis", ytree_type, ytop); + } else if (mode === "treemap" || mode === "sunburst") { + let js; + if (end_col || end_row) { + js = await view.to_json({end_col, end_row, leaves_only: false}); + } else { + js = await view.to_json(); + } + let [charts, , colorRange] = make_tree_data(js, row_pivots, columns, mode === "treemap"); + for (let series of charts) { + let config = default_config.call(this, columns, mode); + config.series = [series]; + if (charts.length > 1) { + config.title.text = series.title; + } + config.plotOptions.series.borderWidth = 1; + config.legend.floating = false; + if (colorRange) { + color_axis.call(this, config, colorRange, restyle); + } + configs.push(config); + } + } else if (mode === "line") { + let s; + let config = (configs[0] = default_config.call(this, columns, mode)); + + if (col_pivots.length === 0) { + let cols; + if (end_col || end_row) { + cols = await view.to_columns({end_col, end_row, leaves_only: true}); + } else { + cols = await view.to_columns(); + } + s = await make_xy_column_data(cols, schema, columns, row_pivots, col_pivots); + } else { + let js; + if (end_col || end_row) { + js = await view.to_json({end_col, end_row, leaves_only: false}); + } else { + js = await view.to_json(); + } + s = await make_xy_data(js, schema, columns, row_pivots, col_pivots); + } + + const series = s[0]; + const xtop = s[1]; + const ytop = s[3]; + + const colors = series.length <= 10 ? COLORS_10 : COLORS_20; + config.legend.floating = series.length <= 20; + config.legend.enabled = col_pivots.length > 0; + config.series = series; + config.plotOptions.scatter.marker = {enabled: false, radius: 0}; + config.colors = colors; + if (set_boost(config, xaxis_type, yaxis_type)) { + delete config.chart["type"]; + } + set_both_axis(config, "xAxis", xaxis_name, xaxis_type, xaxis_type, xtop); + set_both_axis(config, "yAxis", yaxis_name, yaxis_type, yaxis_type, ytop); + } else { + let config = (configs[0] = default_config.call(this, columns, mode)); + let cols; + if (end_col || end_row) { + cols = await view.to_columns({end_col, end_row, leaves_only: false}); + } else { + cols = await view.to_columns(); + } + + let [series, top] = make_y_data(cols, row_pivots); + config.series = series; + config.colors = series.length <= 10 ? COLORS_10 : COLORS_20; + config.legend.enabled = col_pivots.length > 0 || series.length > 1; + config.legend.floating = series.length <= 20; + config.plotOptions.series.dataLabels = { + allowOverlap: false, + padding: 10 + }; + if (mode.indexOf("scatter") > -1 || mode.indexOf("line") > -1) { + set_boost(config, xaxis_type, yaxis_type); + } + set_category_axis(config, "xAxis", xtree_type, top); + Object.assign(config, { + yAxis: { + startOnTick: false, + endOnTick: false, + title: { + text: columns.join(", "), + style: {color: "#666666", fontSize: "14px"} + }, + labels: {overflow: "justify"} + } + }); + } + } finally { + element = get_or_create_element.call(this, el); + if (restyle || this.hasAttribute("updating")) { + element.delete(); + } + } + + element.render(mode, configs, this); + }; + +@bindTemplate(template, style) // eslint-disable-next-line no-unused-vars +class HighchartsElement extends HTMLElement { + constructor() { + super(); + this._charts = []; + } + + connectedCallback() { + this._container = this.shadowRoot.querySelector("#container"); + } + + render(mode, configs, callee) { + if (this._charts.length > 0 && this._charts.length === configs.length) { + let idx = 0; + for (let cidx = 0; cidx < this._charts.length; cidx++) { + const chart = this._charts[cidx]; + let config = configs[idx++]; + if (config.boost) { + let target = chart.renderTo; + try { + chart.destroy(); + } catch (e) { + console.warn("Scatter plot destroy() call failed - this is probably leaking memory"); + } + this._charts[cidx] = Highcharts.chart(target, config); + } else if (mode === "scatter") { + let conf = { + series: config.series, + plotOptions: {} + }; + set_tick_size.call(callee, conf); + chart.update(conf); + } else { + let opts = {series: config.series, xAxis: config.xAxis, yAxis: config.yAxis}; + chart.update(opts); + } + } + } else { + this.delete(); + for (let config of configs) { + let chart = document.createElement("div"); + chart.className = "chart"; + this._container.appendChild(chart); + this._charts.push(() => Highcharts.chart(chart, config)); + } + + for (let i = 0; i < this._charts.length; i++) { + this._charts[i] = this._charts[i](); + } + } + + if (!this._charts.every(x => this._container.contains(x.renderTo))) { + this.remove(); + this._charts.map(x => this._container.appendChild(x.renderTo)); + } + + // TODO resize bug in Highcharts? + if (configs.length > 1) { + this.resize(); + } + + if (detectIE()) { + setTimeout(() => this.resize()); + } + } + + resize() { + if (this._charts && this._charts.length > 0) { + this._charts.map(x => x.reflow()); + } + } + + remove() { + this._charts = []; + for (let e of Array.prototype.slice.call(this._container.children)) { + if (e.tagName === "DIV") { + this._container.removeChild(e); + } + } + } + + delete() { + for (let chart of this._charts) { + try { + chart.destroy(); + } catch (e) { + console.warn("Scatter plot destroy() call failed - this is probably leaking memory"); + } + } + this.remove(); + } +} diff --git a/packages/perspective-viewer-highcharts/src/js/highcharts/highcharts.js b/packages/perspective-viewer-highcharts/src/js/highcharts/highcharts.js index a74ef53bc3..69dc535e75 100644 --- a/packages/perspective-viewer-highcharts/src/js/highcharts/highcharts.js +++ b/packages/perspective-viewer-highcharts/src/js/highcharts/highcharts.js @@ -1,211 +1,211 @@ -/****************************************************************************** - * - * 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 {draw, PRIVATE} from "./draw.js"; -import {registerPlugin} from "@finos/perspective-viewer/dist/esm/utils.js"; - -function resize() { - if (this[PRIVATE]) { - this[PRIVATE].chart.resize(); - } -} - -function delete_chart() { - if (this[PRIVATE]) { - this[PRIVATE].chart.delete(); - } -} - -const MAX_CELL_COUNT = { - line: 25000, - area: 25000, - scatter: 100000, - bubble: 25000, - column: 25000, - treemap: 2500, - sunburst: 1000, - heatmap: 20000 -}; - -const MAX_COLUMN_COUNT = { - line: 100, - area: 100, - scatter: 100, - bubble: 100, - column: 100, - treemap: 24, - sunburst: 24, - heatmap: 24 -}; - -const PLUGINS = { - x_bar: { - name: "X Bar Chart", - create: draw("x_bar", true), - update: draw("x_bar", false), - resize: resize, - initial: { - type: "number", - count: 1 - }, - selectMode: "select", - delete: delete_chart, - max_cells: MAX_CELL_COUNT["column"], - max_columns: MAX_COLUMN_COUNT["column"] - }, - - y_bar: { - name: "Y Bar Chart", - create: draw("y_bar", true), - update: draw("y_bar", false), - resize: resize, - initial: { - type: "number", - count: 1 - }, - selectMode: "select", - delete: delete_chart, - max_cells: MAX_CELL_COUNT["column"], - max_columns: MAX_COLUMN_COUNT["column"] - }, - - y_line: { - name: "Y Line Chart", - create: draw("y_line", true), - update: draw("y_line", false), - resize: resize, - initial: { - type: "number", - count: 1 - }, - selectMode: "select", - delete: delete_chart, - max_cells: MAX_CELL_COUNT["line"], - max_columns: MAX_COLUMN_COUNT["line"] - }, - - y_scatter: { - name: "Y Scatter Chart", - create: draw("y_scatter", true), - update: draw("y_scatter", false), - resize: resize, - initial: { - type: "number", - count: 1 - }, - selectMode: "select", - delete: delete_chart, - max_cells: MAX_CELL_COUNT["scatter"], - max_columns: MAX_COLUMN_COUNT["scatter"] - }, - - y_area: { - name: "Y Area Chart", - create: draw("y_area", true), - update: draw("y_area", false), - resize: resize, - initial: { - type: "number", - count: 1 - }, - selectMode: "select", - delete: delete_chart, - max_cells: MAX_CELL_COUNT["area"], - max_columns: MAX_COLUMN_COUNT["area"] - }, - - xy_line: { - name: "X/Y Line Chart", - create: draw("line", true), - update: draw("line", false), - resize: resize, - initial: { - type: "number", - count: 2, - names: ["X Axis", "Y Axis", "Tooltip"] - }, - selectMode: "toggle", - delete: delete_chart, - max_cells: MAX_CELL_COUNT["scatter"], - max_columns: MAX_COLUMN_COUNT["scatter"] - }, - - xy_scatter: { - name: "X/Y Scatter Chart", - create: draw("scatter", true), - update: draw("scatter", false), - resize: resize, - styleElement: draw("scatter", false, true), - initial: { - type: "number", - count: 2, - names: ["X Axis", "Y Axis", "Color", "Size", "Tooltip"] - }, - selectMode: "toggle", - delete: delete_chart, - max_cells: MAX_CELL_COUNT["scatter"], - max_columns: MAX_COLUMN_COUNT["scatter"] - }, - - treemap: { - name: "Treemap", - create: draw("treemap", true), - update: draw("treemap", false), - resize: resize, - styleElement: draw("treemap", false, true), - initial: { - type: "number", - count: 1, - names: ["Size", "Color"] - }, - selectMode: "toggle", - delete: function() {}, - max_cells: MAX_CELL_COUNT["treemap"], - max_columns: MAX_COLUMN_COUNT["treemap"] - }, - - sunburst: { - name: "Sunburst", - create: draw("sunburst", true), - update: draw("sunburst", false), - styleElement: draw("sunburst", false, true), - resize: resize, - initial: { - type: "number", - count: 1, - names: ["Size", "Color"] - }, - selectMode: "toggle", - delete: function() {}, - max_cells: MAX_CELL_COUNT["sunburst"], - max_columns: MAX_COLUMN_COUNT["sunburst"] - }, - - heatmap: { - name: "Heatmap", - create: draw("heatmap", true), - update: draw("heatmap", false), - resize: resize, - initial: { - type: "number", - count: 1 - }, - selectMode: "select", - delete: delete_chart, - max_cells: MAX_CELL_COUNT["heatmap"], - max_columns: MAX_COLUMN_COUNT["heatmap"] - } -}; - -export default function(...plugins) { - plugins = plugins.length > 0 ? plugins : Object.keys(PLUGINS); - for (const plugin of plugins) { - registerPlugin(plugin, PLUGINS[plugin]); - } -} +/****************************************************************************** + * + * 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 {draw, PRIVATE} from "./draw.js"; +import {registerPlugin} from "@finos/perspective-viewer/dist/esm/utils.js"; + +function resize() { + if (this[PRIVATE]) { + this[PRIVATE].chart.resize(); + } +} + +function delete_chart() { + if (this[PRIVATE]) { + this[PRIVATE].chart.delete(); + } +} + +const MAX_CELL_COUNT = { + line: 25000, + area: 25000, + scatter: 100000, + bubble: 25000, + column: 25000, + treemap: 2500, + sunburst: 1000, + heatmap: 20000 +}; + +const MAX_COLUMN_COUNT = { + line: 100, + area: 100, + scatter: 100, + bubble: 100, + column: 100, + treemap: 24, + sunburst: 24, + heatmap: 24 +}; + +const PLUGINS = { + x_bar: { + name: "X Bar Chart", + create: draw("x_bar", true), + update: draw("x_bar", false), + resize: resize, + initial: { + type: "number", + count: 1 + }, + selectMode: "select", + delete: delete_chart, + max_cells: MAX_CELL_COUNT["column"], + max_columns: MAX_COLUMN_COUNT["column"] + }, + + y_bar: { + name: "Y Bar Chart", + create: draw("y_bar", true), + update: draw("y_bar", false), + resize: resize, + initial: { + type: "number", + count: 1 + }, + selectMode: "select", + delete: delete_chart, + max_cells: MAX_CELL_COUNT["column"], + max_columns: MAX_COLUMN_COUNT["column"] + }, + + y_line: { + name: "Y Line Chart", + create: draw("y_line", true), + update: draw("y_line", false), + resize: resize, + initial: { + type: "number", + count: 1 + }, + selectMode: "select", + delete: delete_chart, + max_cells: MAX_CELL_COUNT["line"], + max_columns: MAX_COLUMN_COUNT["line"] + }, + + y_scatter: { + name: "Y Scatter Chart", + create: draw("y_scatter", true), + update: draw("y_scatter", false), + resize: resize, + initial: { + type: "number", + count: 1 + }, + selectMode: "select", + delete: delete_chart, + max_cells: MAX_CELL_COUNT["scatter"], + max_columns: MAX_COLUMN_COUNT["scatter"] + }, + + y_area: { + name: "Y Area Chart", + create: draw("y_area", true), + update: draw("y_area", false), + resize: resize, + initial: { + type: "number", + count: 1 + }, + selectMode: "select", + delete: delete_chart, + max_cells: MAX_CELL_COUNT["area"], + max_columns: MAX_COLUMN_COUNT["area"] + }, + + xy_line: { + name: "X/Y Line Chart", + create: draw("line", true), + update: draw("line", false), + resize: resize, + initial: { + type: "number", + count: 2, + names: ["X Axis", "Y Axis", "Tooltip"] + }, + selectMode: "toggle", + delete: delete_chart, + max_cells: MAX_CELL_COUNT["scatter"], + max_columns: MAX_COLUMN_COUNT["scatter"] + }, + + xy_scatter: { + name: "X/Y Scatter Chart", + create: draw("scatter", true), + update: draw("scatter", false), + resize: resize, + styleElement: draw("scatter", false, true), + initial: { + type: "number", + count: 2, + names: ["X Axis", "Y Axis", "Color", "Size", "Tooltip"] + }, + selectMode: "toggle", + delete: delete_chart, + max_cells: MAX_CELL_COUNT["scatter"], + max_columns: MAX_COLUMN_COUNT["scatter"] + }, + + treemap: { + name: "Treemap", + create: draw("treemap", true), + update: draw("treemap", false), + resize: resize, + styleElement: draw("treemap", false, true), + initial: { + type: "number", + count: 1, + names: ["Size", "Color"] + }, + selectMode: "toggle", + delete: function() {}, + max_cells: MAX_CELL_COUNT["treemap"], + max_columns: MAX_COLUMN_COUNT["treemap"] + }, + + sunburst: { + name: "Sunburst", + create: draw("sunburst", true), + update: draw("sunburst", false), + styleElement: draw("sunburst", false, true), + resize: resize, + initial: { + type: "number", + count: 1, + names: ["Size", "Color"] + }, + selectMode: "toggle", + delete: function() {}, + max_cells: MAX_CELL_COUNT["sunburst"], + max_columns: MAX_COLUMN_COUNT["sunburst"] + }, + + heatmap: { + name: "Heatmap", + create: draw("heatmap", true), + update: draw("heatmap", false), + resize: resize, + initial: { + type: "number", + count: 1 + }, + selectMode: "select", + delete: delete_chart, + max_cells: MAX_CELL_COUNT["heatmap"], + max_columns: MAX_COLUMN_COUNT["heatmap"] + } +}; + +export default function(...plugins) { + plugins = plugins.length > 0 ? plugins : Object.keys(PLUGINS); + for (const plugin of plugins) { + registerPlugin(plugin, PLUGINS[plugin]); + } +} diff --git a/packages/perspective-viewer-hypergrid/src/js/hypergrid.js b/packages/perspective-viewer-hypergrid/src/js/hypergrid.js index 43b9e2c4a2..15e8f621ed 100644 --- a/packages/perspective-viewer-hypergrid/src/js/hypergrid.js +++ b/packages/perspective-viewer-hypergrid/src/js/hypergrid.js @@ -1,268 +1,268 @@ -/****************************************************************************** - * - * 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 Hypergrid from "faux-hypergrid"; -import Base from "faux-hypergrid/src/Base"; -import Canvas from "faux-hypergrid/src/lib/Canvas"; -import groupedHeaderPlugin from "fin-hypergrid-grouped-header-plugin"; - -import * as perspectivePlugin from "./perspective-plugin"; -import PerspectiveDataModel from "./PerspectiveDataModel"; -import {psp2hypergrid} from "./psp-to-hypergrid"; - -import {bindTemplate, registerPlugin} from "@finos/perspective-viewer/dist/esm/utils.js"; - -import TEMPLATE from "../html/hypergrid.html"; - -import style from "../less/hypergrid.less"; -import {get_styles, clear_styles, get_dynamic_styles, default_grid_properties} from "./styles.js"; -import {set_formatters} from "./formatters.js"; -import {set_editors} from "./editors.js"; -import {treeLineRendererPaint} from "./hypergrid-tree-cell-renderer"; - -Canvas.prototype.stopResizeLoop(); -Canvas.prototype.stopPaintLoop(); - -bindTemplate( - TEMPLATE, - style -)( - class HypergridElement extends HTMLElement { - set_data(data, schema, tschema, row_pivots, columns, force = false) { - const hg_data = psp2hypergrid(data, schema, tschema, row_pivots, columns); - if (this.grid) { - this.grid.behavior.setPSP(hg_data, force); - } else { - this._hg_data = hg_data; - } - } - - connectedCallback() { - if (!this.grid) { - const host = this.shadowRoot.querySelector("#mainGrid"); - - host.setAttribute("hidden", true); - Canvas.prototype.restartPaintLoop(); - this.grid = new Hypergrid(host, {DataModel: PerspectiveDataModel}); - Canvas.prototype.stopPaintLoop(); - host.removeAttribute("hidden"); - this.grid.get_styles = () => get_styles(this); - this.grid.get_dynamic_styles = (...args) => get_dynamic_styles(this, ...args); - - const grid_properties = default_grid_properties(); - const styles = get_styles(this); - grid_properties.renderer = ["SimpleCell", "Borders"]; - - // Handle grouped header plugin bugs - Object.assign(grid_properties.groupedHeader, styles[""].groupedHeader); - if (typeof grid_properties.groupedHeader.flatHeight === "number") { - grid_properties.groupedHeader.flatHeight = grid_properties.groupedHeader.flatHeight.toString(); - } - - this.grid.installPlugins([perspectivePlugin, [groupedHeaderPlugin, grid_properties.groupedHeader]]); - - // Broken in fin-hypergrid-grouped-header 0.1.2 - let _old_paint = this.grid.cellRenderers.items.GroupedHeader.paint; - this.grid.cellRenderers.items.GroupedHeader.paint = function(gc, config) { - this.visibleColumns = config.grid.renderer.visibleColumns; - return _old_paint.call(this, gc, config); - }; - - this.grid.addProperties(grid_properties); - this.grid.addProperties(styles[""]); - - set_formatters(this.grid); - set_editors(this.grid); - - // Add tree cell renderer - this.grid.cellRenderers.add("TreeCell", Base.extend({paint: treeLineRendererPaint})); - - if (this._hg_data) { - this.grid.behavior.setPSP(this._hg_data); - delete this._hgdata; - } - } - } - } -); - -const HYPERGRID_INSTANCE = Symbol("Hypergrid private"); - -async function grid_update(div, view, task) { - const nrows = await view.num_rows(); - if (task.cancelled) { - return; - } - const hypergrid = get_hypergrid.call(this); - if (!hypergrid) { - return; - } - const dataModel = hypergrid.behavior.dataModel; - dataModel._view = view; - dataModel._table = this._table; - dataModel.setDirty(nrows); - hypergrid.behaviorChanged(); - hypergrid.canvas.paintNow(); -} - -function style_element() { - if (this[HYPERGRID_INSTANCE]) { - const element = this[HYPERGRID_INSTANCE]; - clear_styles(element); - const styles = get_styles(element); - if (element.grid) { - element.grid.addProperties(styles[""]); - } - element.grid.behavior.createColumns(); - element.grid.canvas.paintNow(); - } -} - -function get_hypergrid() { - return this[HYPERGRID_INSTANCE] ? this[HYPERGRID_INSTANCE].grid : undefined; -} - -/** - * Create a new web component, and attach it to the DOM. - * - * @param {HTMLElement} div Attachment point. - */ -async function getOrCreateHypergrid(div) { - let perspectiveHypergridElement; - if (!get_hypergrid.call(this)) { - perspectiveHypergridElement = this[HYPERGRID_INSTANCE] = document.createElement("perspective-hypergrid"); - perspectiveHypergridElement.setAttribute("tabindex", 1); - perspectiveHypergridElement.addEventListener("blur", () => { - if (perspectiveHypergridElement.grid && !perspectiveHypergridElement.grid._is_editing) { - perspectiveHypergridElement.grid.selectionModel.clear(true); //keepRowSelections = true - perspectiveHypergridElement.grid.paintNow(); - } - }); - } else { - perspectiveHypergridElement = this[HYPERGRID_INSTANCE]; - } - - if (!perspectiveHypergridElement.isConnected) { - div.innerHTML = ""; - div.appendChild(perspectiveHypergridElement); - await new Promise(resolve => setTimeout(resolve)); - perspectiveHypergridElement.grid.canvas.resize(false); - } - return perspectiveHypergridElement; -} - -function suppress_paint(hypergrid, f) { - const canvas = hypergrid.divCanvas; - hypergrid.divCanvas = undefined; - f(); - hypergrid.divCanvas = canvas; -} - -async function grid_create(div, view, task, max_rows, max_cols, force) { - let hypergrid = get_hypergrid.call(this); - if (hypergrid) { - hypergrid.behavior.dataModel._view = undefined; - hypergrid.behavior.dataModel._table = undefined; - suppress_paint(hypergrid, () => hypergrid.allowEvents(false)); - } - - const config = await view.get_config(); - - if (task.cancelled) { - return; - } - const colPivots = config.column_pivots; - const rowPivots = config.row_pivots; - const data_window = { - start_row: 0, - end_row: 1, - id: rowPivots.length === 0 && colPivots.length === 0 - }; - - const [nrows, json, schema, tschema, all_columns] = await Promise.all([view.num_rows(), view.to_columns(data_window), view.schema(), this._table.schema(), view.column_paths()]); - - if (task.cancelled) { - return; - } - - let perspectiveHypergridElement = await getOrCreateHypergrid.call(this, div); - hypergrid = get_hypergrid.call(this); - - if (task.cancelled) { - return; - } - - const columns = all_columns.filter(x => x !== "__INDEX__"); - const dataModel = hypergrid.behavior.dataModel; - dataModel._grid = hypergrid; - - dataModel.setIsTree(rowPivots.length > 0); - dataModel.setDirty(nrows); - dataModel.clearSelectionState(); - dataModel._view = view; - dataModel._table = this._table; - dataModel._config = config; - dataModel._viewer = this; - dataModel._columns = columns; - dataModel._pad_window = this.hasAttribute("settings"); - - hypergrid.renderer.needsComputeCellsBounds = true; - suppress_paint(hypergrid, () => perspectiveHypergridElement.set_data(json, schema, tschema, rowPivots, columns, force)); - if (hypergrid.behavior.dataModel._outstanding) { - await hypergrid.behavior.dataModel._outstanding.req; - } - - if (this._plugin_config) { - suppress_paint(hypergrid, () => dataModel.setSelectedRowID(this._plugin_config.selected)); - delete this._plugin_config; - } - - await hypergrid.canvas.resize(true); - hypergrid.allowEvents(true); -} - -const plugin = { - name: "Grid", - create: grid_create, - selectMode: "toggle", - update: grid_update, - deselectMode: "pivots", - styleElement: style_element, - save: function() { - const hypergrid = get_hypergrid.call(this); - if (hypergrid && hypergrid.selectionModel.hasRowSelections()) { - return {selected: hypergrid.behavior.dataModel.getSelectedRowID()}; - } - }, - restore: function(config) { - this._plugin_config = config; - }, - resize: async function() { - const hypergrid = get_hypergrid.call(this); - if (hypergrid) { - let nrows = await this._view.num_rows(); - hypergrid.behavior.dataModel.setDirty(nrows); - await hypergrid.canvas.resize(true); - } - }, - delete: function() { - const hypergrid = get_hypergrid.call(this); - if (hypergrid) { - hypergrid.terminate(); - hypergrid.div = undefined; - hypergrid.canvas.div = undefined; - hypergrid.canvas.canvas = undefined; - hypergrid.sbVScroller = undefined; - hypergrid.sbHScroller = undefined; - delete this[HYPERGRID_INSTANCE]; - } - } -}; - -registerPlugin("hypergrid", plugin); +/****************************************************************************** + * + * 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 Hypergrid from "faux-hypergrid"; +import Base from "faux-hypergrid/src/Base"; +import Canvas from "faux-hypergrid/src/lib/Canvas"; +import groupedHeaderPlugin from "fin-hypergrid-grouped-header-plugin"; + +import * as perspectivePlugin from "./perspective-plugin"; +import PerspectiveDataModel from "./PerspectiveDataModel"; +import {psp2hypergrid} from "./psp-to-hypergrid"; + +import {bindTemplate, registerPlugin} from "@finos/perspective-viewer/dist/esm/utils.js"; + +import TEMPLATE from "../html/hypergrid.html"; + +import style from "../less/hypergrid.less"; +import {get_styles, clear_styles, get_dynamic_styles, default_grid_properties} from "./styles.js"; +import {set_formatters} from "./formatters.js"; +import {set_editors} from "./editors.js"; +import {treeLineRendererPaint} from "./hypergrid-tree-cell-renderer"; + +Canvas.prototype.stopResizeLoop(); +Canvas.prototype.stopPaintLoop(); + +bindTemplate( + TEMPLATE, + style +)( + class HypergridElement extends HTMLElement { + set_data(data, schema, tschema, row_pivots, columns, force = false) { + const hg_data = psp2hypergrid(data, schema, tschema, row_pivots, columns); + if (this.grid) { + this.grid.behavior.setPSP(hg_data, force); + } else { + this._hg_data = hg_data; + } + } + + connectedCallback() { + if (!this.grid) { + const host = this.shadowRoot.querySelector("#mainGrid"); + + host.setAttribute("hidden", true); + Canvas.prototype.restartPaintLoop(); + this.grid = new Hypergrid(host, {DataModel: PerspectiveDataModel}); + Canvas.prototype.stopPaintLoop(); + host.removeAttribute("hidden"); + this.grid.get_styles = () => get_styles(this); + this.grid.get_dynamic_styles = (...args) => get_dynamic_styles(this, ...args); + + const grid_properties = default_grid_properties(); + const styles = get_styles(this); + grid_properties.renderer = ["SimpleCell", "Borders"]; + + // Handle grouped header plugin bugs + Object.assign(grid_properties.groupedHeader, styles[""].groupedHeader); + if (typeof grid_properties.groupedHeader.flatHeight === "number") { + grid_properties.groupedHeader.flatHeight = grid_properties.groupedHeader.flatHeight.toString(); + } + + this.grid.installPlugins([perspectivePlugin, [groupedHeaderPlugin, grid_properties.groupedHeader]]); + + // Broken in fin-hypergrid-grouped-header 0.1.2 + let _old_paint = this.grid.cellRenderers.items.GroupedHeader.paint; + this.grid.cellRenderers.items.GroupedHeader.paint = function(gc, config) { + this.visibleColumns = config.grid.renderer.visibleColumns; + return _old_paint.call(this, gc, config); + }; + + this.grid.addProperties(grid_properties); + this.grid.addProperties(styles[""]); + + set_formatters(this.grid); + set_editors(this.grid); + + // Add tree cell renderer + this.grid.cellRenderers.add("TreeCell", Base.extend({paint: treeLineRendererPaint})); + + if (this._hg_data) { + this.grid.behavior.setPSP(this._hg_data); + delete this._hgdata; + } + } + } + } +); + +const HYPERGRID_INSTANCE = Symbol("Hypergrid private"); + +async function grid_update(div, view, task) { + const nrows = await view.num_rows(); + if (task.cancelled) { + return; + } + const hypergrid = get_hypergrid.call(this); + if (!hypergrid) { + return; + } + const dataModel = hypergrid.behavior.dataModel; + dataModel._view = view; + dataModel._table = this._table; + dataModel.setDirty(nrows); + hypergrid.behaviorChanged(); + hypergrid.canvas.paintNow(); +} + +function style_element() { + if (this[HYPERGRID_INSTANCE]) { + const element = this[HYPERGRID_INSTANCE]; + clear_styles(element); + const styles = get_styles(element); + if (element.grid) { + element.grid.addProperties(styles[""]); + } + element.grid.behavior.createColumns(); + element.grid.canvas.paintNow(); + } +} + +function get_hypergrid() { + return this[HYPERGRID_INSTANCE] ? this[HYPERGRID_INSTANCE].grid : undefined; +} + +/** + * Create a new web component, and attach it to the DOM. + * + * @param {HTMLElement} div Attachment point. + */ +async function getOrCreateHypergrid(div) { + let perspectiveHypergridElement; + if (!get_hypergrid.call(this)) { + perspectiveHypergridElement = this[HYPERGRID_INSTANCE] = document.createElement("perspective-hypergrid"); + perspectiveHypergridElement.setAttribute("tabindex", 1); + perspectiveHypergridElement.addEventListener("blur", () => { + if (perspectiveHypergridElement.grid && !perspectiveHypergridElement.grid._is_editing) { + perspectiveHypergridElement.grid.selectionModel.clear(true); //keepRowSelections = true + perspectiveHypergridElement.grid.paintNow(); + } + }); + } else { + perspectiveHypergridElement = this[HYPERGRID_INSTANCE]; + } + + if (!perspectiveHypergridElement.isConnected) { + div.innerHTML = ""; + div.appendChild(perspectiveHypergridElement); + await new Promise(resolve => setTimeout(resolve)); + perspectiveHypergridElement.grid.canvas.resize(false); + } + return perspectiveHypergridElement; +} + +function suppress_paint(hypergrid, f) { + const canvas = hypergrid.divCanvas; + hypergrid.divCanvas = undefined; + f(); + hypergrid.divCanvas = canvas; +} + +async function grid_create(div, view, task, max_rows, max_cols, force) { + let hypergrid = get_hypergrid.call(this); + if (hypergrid) { + hypergrid.behavior.dataModel._view = undefined; + hypergrid.behavior.dataModel._table = undefined; + suppress_paint(hypergrid, () => hypergrid.allowEvents(false)); + } + + const config = await view.get_config(); + + if (task.cancelled) { + return; + } + const colPivots = config.column_pivots; + const rowPivots = config.row_pivots; + const data_window = { + start_row: 0, + end_row: 1, + id: rowPivots.length === 0 && colPivots.length === 0 + }; + + const [nrows, json, schema, tschema, all_columns] = await Promise.all([view.num_rows(), view.to_columns(data_window), view.schema(), this._table.schema(), view.column_paths()]); + + if (task.cancelled) { + return; + } + + let perspectiveHypergridElement = await getOrCreateHypergrid.call(this, div); + hypergrid = get_hypergrid.call(this); + + if (task.cancelled) { + return; + } + + const columns = all_columns.filter(x => x !== "__INDEX__"); + const dataModel = hypergrid.behavior.dataModel; + dataModel._grid = hypergrid; + + dataModel.setIsTree(rowPivots.length > 0); + dataModel.setDirty(nrows); + dataModel.clearSelectionState(); + dataModel._view = view; + dataModel._table = this._table; + dataModel._config = config; + dataModel._viewer = this; + dataModel._columns = columns; + dataModel._pad_window = this.hasAttribute("settings"); + + hypergrid.renderer.needsComputeCellsBounds = true; + suppress_paint(hypergrid, () => perspectiveHypergridElement.set_data(json, schema, tschema, rowPivots, columns, force)); + if (hypergrid.behavior.dataModel._outstanding) { + await hypergrid.behavior.dataModel._outstanding.req; + } + + if (this._plugin_config) { + suppress_paint(hypergrid, () => dataModel.setSelectedRowID(this._plugin_config.selected)); + delete this._plugin_config; + } + + await hypergrid.canvas.resize(true); + hypergrid.allowEvents(true); +} + +const plugin = { + name: "Grid", + create: grid_create, + selectMode: "toggle", + update: grid_update, + deselectMode: "pivots", + styleElement: style_element, + save: function() { + const hypergrid = get_hypergrid.call(this); + if (hypergrid && hypergrid.selectionModel.hasRowSelections()) { + return {selected: hypergrid.behavior.dataModel.getSelectedRowID()}; + } + }, + restore: function(config) { + this._plugin_config = config; + }, + resize: async function() { + const hypergrid = get_hypergrid.call(this); + if (hypergrid) { + let nrows = await this._view.num_rows(); + hypergrid.behavior.dataModel.setDirty(nrows); + await hypergrid.canvas.resize(true); + } + }, + delete: function() { + const hypergrid = get_hypergrid.call(this); + if (hypergrid) { + hypergrid.terminate(); + hypergrid.div = undefined; + hypergrid.canvas.div = undefined; + hypergrid.canvas.canvas = undefined; + hypergrid.sbVScroller = undefined; + hypergrid.sbHScroller = undefined; + delete this[HYPERGRID_INSTANCE]; + } + } +}; + +registerPlugin("hypergrid", plugin); diff --git a/packages/perspective-viewer/src/html/computed_expression_widget.html b/packages/perspective-viewer/src/html/computed_expression_widget.html index ec5cdaf7ce..ee0076d9ab 100644 --- a/packages/perspective-viewer/src/html/computed_expression_widget.html +++ b/packages/perspective-viewer/src/html/computed_expression_widget.html @@ -15,12 +15,7 @@ New Column
- +
diff --git a/packages/perspective-viewer/src/js/row.js b/packages/perspective-viewer/src/js/row.js index 5eccd22a5f..8e1c55d972 100644 --- a/packages/perspective-viewer/src/js/row.js +++ b/packages/perspective-viewer/src/js/row.js @@ -1,294 +1,294 @@ -/****************************************************************************** - * - * 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 debounce from "lodash/debounce"; - -import Awesomplete from "awesomplete"; -import awesomplete_style from "!!css-loader!awesomplete/awesomplete.css"; - -import {bindTemplate} from "./utils.js"; - -import perspective from "@finos/perspective"; -import {get_type_config} from "@finos/perspective/dist/esm/config"; -import template from "../html/row.html"; - -import style from "../less/row.less"; -import {html, render, nothing} from "lit-html"; - -const SPAN = document.createElement("span"); -SPAN.style.visibility = "hidden"; -SPAN.style.fontFamily = "monospace"; -SPAN.style.fontSize = "12px"; -SPAN.style.position = "absolute"; - -function get_text_width(text, max = 0) { - // FIXME get these values form the stylesheet - SPAN.innerHTML = text; - document.body.appendChild(SPAN); - const width = `${Math.max(max, SPAN.offsetWidth) + 20}px`; - document.body.removeChild(SPAN); - return width; -} - -// Eslint complains here because we don't do anything, but actually we globally -// register this class as a CustomElement -@bindTemplate(template, {toString: () => style + "\n" + awesomplete_style}) // eslint-disable-next-line no-unused-vars -class Row extends HTMLElement { - set name(n) { - const elem = this.shadowRoot.querySelector("#name"); - elem.innerHTML = this.getAttribute("name"); - } - - _option_template(agg, name) { - return html` - - `; - } - - _select_template(category, type) { - const items = perspective[category][type] || []; - const weighted_options = html` - - ${this._weights.map(x => this._option_template(JSON.stringify(["weighted mean", x]), x))} - - `; - const has_weighted_mean = category === "TYPE_AGGREGATES" && (type === "integer" || type === "float"); - return html` - ${items.map(x => this._option_template(x))} ${has_weighted_mean ? weighted_options : nothing} - `; - } - - set_weights(xs) { - this._weights = xs; - } - - set type(t) { - const elem = this.shadowRoot.querySelector("#name"); - const type = this.getAttribute("type"); - if (!type) return; - const type_config = get_type_config(type); - if (type_config.type) { - elem.classList.add(type_config.type); - } - elem.classList.add(type); - const agg_dropdown = this.shadowRoot.querySelector("#column_aggregate"); - const filter_dropdown = this.shadowRoot.querySelector("#filter_operator"); - - render(this._select_template("TYPE_AGGREGATES", type_config.type || type), agg_dropdown); - render(this._select_template("TYPE_FILTERS", type_config.type || type), filter_dropdown); - - if (!this.hasAttribute("aggregate")) { - this.aggregate = type_config.aggregate; - } else { - this.aggregate = this.getAttribute("aggregate"); - } - if (this.hasAttribute("filter")) { - this.filter = this.getAttribute("filter"); - } - - const filter_operand = this.shadowRoot.querySelector("#filter_operand"); - this._callback = event => this._update_filter(event); - filter_operand.addEventListener("keyup", this._callback.bind(this)); - } - - choices(choices) { - const filter_operand = this.shadowRoot.querySelector("#filter_operand"); - const filter_operator = this.shadowRoot.querySelector("#filter_operator"); - const selector = new Awesomplete(filter_operand, { - label: this.getAttribute("name"), - list: choices, - minChars: 0, - autoFirst: true, - filter: function(text, input) { - return Awesomplete.FILTER_CONTAINS(text, input.match(/[^,]*$/)[0]); - }, - item: function(text, input) { - return Awesomplete.ITEM(text, input.match(/[^,]*$/)[0]); - }, - replace: function(text) { - const before = this.input.value.match(/^.+,\s*|/)[0]; - if (filter_operator.value === "in" || filter_operator.value === "not in") { - this.input.value = before + text + ", "; - } else { - this.input.value = before + text; - } - } - }); - if (filter_operand.value === "") { - selector.evaluate(); - } - filter_operand.focus(); - this._filter_operand.addEventListener("focus", () => { - if (filter_operand.value.trim().length === 0) { - selector.evaluate(); - } - }); - filter_operand.addEventListener("awesomplete-selectcomplete", this._callback); - } - - set filter(f) { - const filter_dropdown = this.shadowRoot.querySelector("#filter_operator"); - const filter = JSON.parse(this.getAttribute("filter")); - if (filter_dropdown.value !== filter.operator) { - filter_dropdown.value = filter.operator || get_type_config(this.getAttribute("type")).filter_operator; - } - filter_dropdown.style.width = get_text_width(filter_dropdown.value); - const filter_input = this.shadowRoot.querySelector("#filter_operand"); - const operand = filter.operand ? filter.operand.toString() : ""; - if (!this._initialized) { - filter_input.value = operand; - } - if (filter_dropdown.value === perspective.FILTER_OPERATORS.isNull || filter_dropdown.value === perspective.FILTER_OPERATORS.isNotNull) { - filter_input.style.display = "none"; - } else { - filter_input.style.display = "inline-block"; - filter_input.style.width = get_text_width(operand, 30); - } - } - - set aggregate(a) { - const agg_dropdown = this.shadowRoot.querySelector("#column_aggregate"); - const aggregate = this.getAttribute("aggregate"); - if (agg_dropdown.value !== aggregate && this.hasAttribute("type")) { - const type = this.getAttribute("type"); - agg_dropdown.value = aggregate || get_type_config(type).aggregate; - } - this._blur_agg_dropdown(); - } - - set computed_column(c) { - // const data = this._get_computed_data(); - // const computed_input_column = - // this.shadowRoot.querySelector('#computed_input_column'); - // const computation_name = - // this.shadowRoot.querySelector('#computation_name'); - // computation_name.textContent = data.computation.name; - // computed_input_column.textContent = data.input_column; - } - - _get_computed_data() { - const data = JSON.parse(this.getAttribute("computed_column")); - return { - column_name: data.column_name, - input_columns: data.input_columns, - input_type: data.input_type, - computation: data.computation, - type: data.type - }; - } - - _update_filter(event) { - const filter_operand = this.shadowRoot.querySelector("#filter_operand"); - const filter_operator = this.shadowRoot.querySelector("#filter_operator"); - let val = filter_operand.value; - const type = this.getAttribute("type"); - switch (type) { - case "float": - val = parseFloat(val); - break; - case "integer": - val = parseInt(val); - break; - case "boolean": - val = val.toLowerCase().indexOf("true") > -1; - break; - case "string": - default: - } - if (filter_operator.value === perspective.FILTER_OPERATORS.isIn || filter_operator.value === perspective.FILTER_OPERATORS.isNotIn) { - val = val.split(",").map(x => x.trim()); - } - this.setAttribute("filter", JSON.stringify({operator: filter_operator.value, operand: val})); - this.dispatchEvent(new CustomEvent("filter-selected", {detail: event})); - } - - _set_data_transfer(event) { - if (this.hasAttribute("filter")) { - const {operator, operand} = JSON.parse(this.getAttribute("filter")); - event.dataTransfer.setData("text", JSON.stringify([this.getAttribute("name"), operator, operand, this.getAttribute("type"), this.getAttribute("aggregate")])); - } else { - event.dataTransfer.setData( - "text", - JSON.stringify([this.getAttribute("name"), get_type_config(this.getAttribute("type")).filter_operator, undefined, this.getAttribute("type"), this.getAttribute("aggregate")]) - ); - } - this.dispatchEvent(new CustomEvent("row-drag")); - } - - _register_ids() { - this._li = this.shadowRoot.querySelector(".row_draggable"); - this._visible = this.shadowRoot.querySelector(".is_visible"); - this._row_close = this.shadowRoot.querySelector("#row_close"); - this._agg_dropdown = this.shadowRoot.querySelector("#column_aggregate"); - this._sort_order = this.shadowRoot.querySelector("#sort_order"); - this._filter_operand = this.shadowRoot.querySelector("#filter_operand"); - this._filter_operator = this.shadowRoot.querySelector("#filter_operator"); - this._edit_computed_column_button = this.shadowRoot.querySelector("#row_edit"); - this._column_aggregate_category = this.shadowRoot.querySelector("#column_aggregate_category"); - } - - _blur_agg_dropdown() { - this._agg_dropdown.blur(); - if (this._agg_dropdown.value[0] === "[") { - for (const option of this._agg_dropdown.querySelectorAll("optgroup option")) { - const name = option.getAttribute("data-desc"); - option.innerHTML = `mean by ${name}`; - } - } - } - - _focus_agg_dropdown() { - for (const option of this._agg_dropdown.querySelectorAll("optgroup option")) { - const name = option.getAttribute("data-desc"); - option.innerHTML = `by ${name}`; - } - } - - _register_callbacks() { - this._li.addEventListener("dragstart", this._set_data_transfer.bind(this)); - this._li.addEventListener("dragend", () => { - this.dispatchEvent(new CustomEvent("row-dragend")); - }); - this._visible.addEventListener("mousedown", event => this.dispatchEvent(new CustomEvent("visibility-clicked", {detail: event}))); - this._row_close.addEventListener("mousedown", event => this.dispatchEvent(new CustomEvent("close-clicked", {detail: event}))); - this._agg_dropdown.addEventListener("focus", this._focus_agg_dropdown.bind(this)); - - this._agg_dropdown.addEventListener("change", event => { - this._blur_agg_dropdown(); - const value = this._agg_dropdown.value; - this.setAttribute("aggregate", value); - this.dispatchEvent(new CustomEvent("aggregate-selected", {detail: event})); - }); - this._sort_order.addEventListener("click", event => { - this.dispatchEvent(new CustomEvent("sort-order", {detail: event})); - }); - - const debounced_filter = debounce(event => this._update_filter(event), 50); - this._filter_operator.addEventListener("change", () => { - this._filter_operand.focus(); - this._filter_operator.style.width = get_text_width(this._filter_operator.value); - const filter_input = this.shadowRoot.querySelector("#filter_operand"); - filter_input.style.width = get_text_width("" + this._filter_operand.value, 30); - debounced_filter(); - }); - this._edit_computed_column_button.addEventListener("click", () => { - this.dispatchEvent( - new CustomEvent("perspective-computed-column-edit", { - bubbles: true, - detail: this._get_computed_data() - }) - ); - }); - } - - connectedCallback() { - this._register_ids(); - this._register_callbacks(); - } -} +/****************************************************************************** + * + * 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 debounce from "lodash/debounce"; + +import Awesomplete from "awesomplete"; +import awesomplete_style from "!!css-loader!awesomplete/awesomplete.css"; + +import {bindTemplate} from "./utils.js"; + +import perspective from "@finos/perspective"; +import {get_type_config} from "@finos/perspective/dist/esm/config"; +import template from "../html/row.html"; + +import style from "../less/row.less"; +import {html, render, nothing} from "lit-html"; + +const SPAN = document.createElement("span"); +SPAN.style.visibility = "hidden"; +SPAN.style.fontFamily = "monospace"; +SPAN.style.fontSize = "12px"; +SPAN.style.position = "absolute"; + +function get_text_width(text, max = 0) { + // FIXME get these values form the stylesheet + SPAN.innerHTML = text; + document.body.appendChild(SPAN); + const width = `${Math.max(max, SPAN.offsetWidth) + 20}px`; + document.body.removeChild(SPAN); + return width; +} + +// Eslint complains here because we don't do anything, but actually we globally +// register this class as a CustomElement +@bindTemplate(template, {toString: () => style + "\n" + awesomplete_style}) // eslint-disable-next-line no-unused-vars +class Row extends HTMLElement { + set name(n) { + const elem = this.shadowRoot.querySelector("#name"); + elem.innerHTML = this.getAttribute("name"); + } + + _option_template(agg, name) { + return html` + + `; + } + + _select_template(category, type) { + const items = perspective[category][type] || []; + const weighted_options = html` + + ${this._weights.map(x => this._option_template(JSON.stringify(["weighted mean", x]), x))} + + `; + const has_weighted_mean = category === "TYPE_AGGREGATES" && (type === "integer" || type === "float"); + return html` + ${items.map(x => this._option_template(x))} ${has_weighted_mean ? weighted_options : nothing} + `; + } + + set_weights(xs) { + this._weights = xs; + } + + set type(t) { + const elem = this.shadowRoot.querySelector("#name"); + const type = this.getAttribute("type"); + if (!type) return; + const type_config = get_type_config(type); + if (type_config.type) { + elem.classList.add(type_config.type); + } + elem.classList.add(type); + const agg_dropdown = this.shadowRoot.querySelector("#column_aggregate"); + const filter_dropdown = this.shadowRoot.querySelector("#filter_operator"); + + render(this._select_template("TYPE_AGGREGATES", type_config.type || type), agg_dropdown); + render(this._select_template("TYPE_FILTERS", type_config.type || type), filter_dropdown); + + if (!this.hasAttribute("aggregate")) { + this.aggregate = type_config.aggregate; + } else { + this.aggregate = this.getAttribute("aggregate"); + } + if (this.hasAttribute("filter")) { + this.filter = this.getAttribute("filter"); + } + + const filter_operand = this.shadowRoot.querySelector("#filter_operand"); + this._callback = event => this._update_filter(event); + filter_operand.addEventListener("keyup", this._callback.bind(this)); + } + + choices(choices) { + const filter_operand = this.shadowRoot.querySelector("#filter_operand"); + const filter_operator = this.shadowRoot.querySelector("#filter_operator"); + const selector = new Awesomplete(filter_operand, { + label: this.getAttribute("name"), + list: choices, + minChars: 0, + autoFirst: true, + filter: function(text, input) { + return Awesomplete.FILTER_CONTAINS(text, input.match(/[^,]*$/)[0]); + }, + item: function(text, input) { + return Awesomplete.ITEM(text, input.match(/[^,]*$/)[0]); + }, + replace: function(text) { + const before = this.input.value.match(/^.+,\s*|/)[0]; + if (filter_operator.value === "in" || filter_operator.value === "not in") { + this.input.value = before + text + ", "; + } else { + this.input.value = before + text; + } + } + }); + if (filter_operand.value === "") { + selector.evaluate(); + } + filter_operand.focus(); + this._filter_operand.addEventListener("focus", () => { + if (filter_operand.value.trim().length === 0) { + selector.evaluate(); + } + }); + filter_operand.addEventListener("awesomplete-selectcomplete", this._callback); + } + + set filter(f) { + const filter_dropdown = this.shadowRoot.querySelector("#filter_operator"); + const filter = JSON.parse(this.getAttribute("filter")); + if (filter_dropdown.value !== filter.operator) { + filter_dropdown.value = filter.operator || get_type_config(this.getAttribute("type")).filter_operator; + } + filter_dropdown.style.width = get_text_width(filter_dropdown.value); + const filter_input = this.shadowRoot.querySelector("#filter_operand"); + const operand = filter.operand ? filter.operand.toString() : ""; + if (!this._initialized) { + filter_input.value = operand; + } + if (filter_dropdown.value === perspective.FILTER_OPERATORS.isNull || filter_dropdown.value === perspective.FILTER_OPERATORS.isNotNull) { + filter_input.style.display = "none"; + } else { + filter_input.style.display = "inline-block"; + filter_input.style.width = get_text_width(operand, 30); + } + } + + set aggregate(a) { + const agg_dropdown = this.shadowRoot.querySelector("#column_aggregate"); + const aggregate = this.getAttribute("aggregate"); + if (agg_dropdown.value !== aggregate && this.hasAttribute("type")) { + const type = this.getAttribute("type"); + agg_dropdown.value = aggregate || get_type_config(type).aggregate; + } + this._blur_agg_dropdown(); + } + + set computed_column(c) { + // const data = this._get_computed_data(); + // const computed_input_column = + // this.shadowRoot.querySelector('#computed_input_column'); + // const computation_name = + // this.shadowRoot.querySelector('#computation_name'); + // computation_name.textContent = data.computation.name; + // computed_input_column.textContent = data.input_column; + } + + _get_computed_data() { + const data = JSON.parse(this.getAttribute("computed_column")); + return { + column_name: data.column_name, + input_columns: data.input_columns, + input_type: data.input_type, + computation: data.computation, + type: data.type + }; + } + + _update_filter(event) { + const filter_operand = this.shadowRoot.querySelector("#filter_operand"); + const filter_operator = this.shadowRoot.querySelector("#filter_operator"); + let val = filter_operand.value; + const type = this.getAttribute("type"); + switch (type) { + case "float": + val = parseFloat(val); + break; + case "integer": + val = parseInt(val); + break; + case "boolean": + val = val.toLowerCase().indexOf("true") > -1; + break; + case "string": + default: + } + if (filter_operator.value === perspective.FILTER_OPERATORS.isIn || filter_operator.value === perspective.FILTER_OPERATORS.isNotIn) { + val = val.split(",").map(x => x.trim()); + } + this.setAttribute("filter", JSON.stringify({operator: filter_operator.value, operand: val})); + this.dispatchEvent(new CustomEvent("filter-selected", {detail: event})); + } + + _set_data_transfer(event) { + if (this.hasAttribute("filter")) { + const {operator, operand} = JSON.parse(this.getAttribute("filter")); + event.dataTransfer.setData("text", JSON.stringify([this.getAttribute("name"), operator, operand, this.getAttribute("type"), this.getAttribute("aggregate")])); + } else { + event.dataTransfer.setData( + "text", + JSON.stringify([this.getAttribute("name"), get_type_config(this.getAttribute("type")).filter_operator, undefined, this.getAttribute("type"), this.getAttribute("aggregate")]) + ); + } + this.dispatchEvent(new CustomEvent("row-drag")); + } + + _register_ids() { + this._li = this.shadowRoot.querySelector(".row_draggable"); + this._visible = this.shadowRoot.querySelector(".is_visible"); + this._row_close = this.shadowRoot.querySelector("#row_close"); + this._agg_dropdown = this.shadowRoot.querySelector("#column_aggregate"); + this._sort_order = this.shadowRoot.querySelector("#sort_order"); + this._filter_operand = this.shadowRoot.querySelector("#filter_operand"); + this._filter_operator = this.shadowRoot.querySelector("#filter_operator"); + this._edit_computed_column_button = this.shadowRoot.querySelector("#row_edit"); + this._column_aggregate_category = this.shadowRoot.querySelector("#column_aggregate_category"); + } + + _blur_agg_dropdown() { + this._agg_dropdown.blur(); + if (this._agg_dropdown.value[0] === "[") { + for (const option of this._agg_dropdown.querySelectorAll("optgroup option")) { + const name = option.getAttribute("data-desc"); + option.innerHTML = `mean by ${name}`; + } + } + } + + _focus_agg_dropdown() { + for (const option of this._agg_dropdown.querySelectorAll("optgroup option")) { + const name = option.getAttribute("data-desc"); + option.innerHTML = `by ${name}`; + } + } + + _register_callbacks() { + this._li.addEventListener("dragstart", this._set_data_transfer.bind(this)); + this._li.addEventListener("dragend", () => { + this.dispatchEvent(new CustomEvent("row-dragend")); + }); + this._visible.addEventListener("mousedown", event => this.dispatchEvent(new CustomEvent("visibility-clicked", {detail: event}))); + this._row_close.addEventListener("mousedown", event => this.dispatchEvent(new CustomEvent("close-clicked", {detail: event}))); + this._agg_dropdown.addEventListener("focus", this._focus_agg_dropdown.bind(this)); + + this._agg_dropdown.addEventListener("change", event => { + this._blur_agg_dropdown(); + const value = this._agg_dropdown.value; + this.setAttribute("aggregate", value); + this.dispatchEvent(new CustomEvent("aggregate-selected", {detail: event})); + }); + this._sort_order.addEventListener("click", event => { + this.dispatchEvent(new CustomEvent("sort-order", {detail: event})); + }); + + const debounced_filter = debounce(event => this._update_filter(event), 50); + this._filter_operator.addEventListener("change", () => { + this._filter_operand.focus(); + this._filter_operator.style.width = get_text_width(this._filter_operator.value); + const filter_input = this.shadowRoot.querySelector("#filter_operand"); + filter_input.style.width = get_text_width("" + this._filter_operand.value, 30); + debounced_filter(); + }); + this._edit_computed_column_button.addEventListener("click", () => { + this.dispatchEvent( + new CustomEvent("perspective-computed-column-edit", { + bubbles: true, + detail: this._get_computed_data() + }) + ); + }); + } + + connectedCallback() { + this._register_ids(); + this._register_callbacks(); + } +} diff --git a/packages/perspective-viewer/src/less/autocomplete_widget.less b/packages/perspective-viewer/src/less/autocomplete_widget.less index 00015db830..604125abe9 100644 --- a/packages/perspective-viewer/src/less/autocomplete_widget.less +++ b/packages/perspective-viewer/src/less/autocomplete_widget.less @@ -24,7 +24,7 @@ overflow-x: hidden; overflow-y: hidden; word-break: break-word; - + // Single-line expressions where the autocomplete follows the caret, // and is positioned by the `reposition` method &.undocked { @@ -43,7 +43,7 @@ } } } - + // Multi-line expressions where the autocomplete is static &.docked { background: var(--plugin--background, none); @@ -63,7 +63,7 @@ min-width: 150px; max-width: 200px; } - + div.psp-autocomplete__item { border-bottom: 1px solid @border-color; } @@ -107,22 +107,29 @@ font-size: 10px; } } - + div.psp-autocomplete__item { display: block; overflow-x: auto; padding: 5px; word-break: keep-all; - + &:hover { // Default colors are the same as Awesomeplete defaults to // achieve consistent look. - background: var(--autocomplete-hover-background, hsl(200, 40%, 80%)); + background: var( + --autocomplete-hover-background, + hsl(200, 40%, 80%) + ); cursor: pointer; } - - &:focus, &[aria-selected="true"]{ - background: var(--autocomplete-select-background, hsl(205, 40%, 40%)); + + &:focus, + &[aria-selected="true"] { + background: var( + --autocomplete-select-background, + hsl(205, 40%, 40%) + ); color: var(--color, #fff); cursor: pointer; @@ -132,12 +139,17 @@ } span.psp-autocomplete-item__label { - &.psp-autocomplete-item__label--column-name { - font-family: var(--interface--font-family, @sans-serif-fonts); - + font-family: var( + --interface--font-family, + @sans-serif-fonts + ); + &:before { - font-family: var(--interface-monospace--font-family, monospace); + font-family: var( + --interface-monospace--font-family, + monospace + ); position: relative; display: inline-block; // display: var(--name-before-display, none); @@ -145,7 +157,7 @@ min-width: 18px; width: var(--column_type--width, auto); } - + &.integer:before, &.float:before { content: var( @@ -157,7 +169,7 @@ var(--column-type--color, #016bc6) ); } - + &.string:before { content: var( --string--column-type--content, @@ -168,7 +180,7 @@ var(--column-type--color, #fe9292) ); } - + &.boolean:before { content: var( --boolean--column-type--content, @@ -179,7 +191,7 @@ var(--column-type--color, #999999) ); } - + &.date:before { content: var( --date--column-type--content, @@ -190,7 +202,7 @@ var(--column-type--color, #999999) ); } - + &.datetime:before { content: var( --datetime--column-type--content, @@ -205,4 +217,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/perspective-viewer/src/less/computed_expression_widget.less b/packages/perspective-viewer/src/less/computed_expression_widget.less index 4002716dd1..aabfa590f8 100644 --- a/packages/perspective-viewer/src/less/computed_expression_widget.less +++ b/packages/perspective-viewer/src/less/computed_expression_widget.less @@ -167,4 +167,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/perspective-viewer/src/less/default.less b/packages/perspective-viewer/src/less/default.less index f5de277a54..3354c9f452 100644 --- a/packages/perspective-viewer/src/less/default.less +++ b/packages/perspective-viewer/src/less/default.less @@ -213,7 +213,8 @@ } #active_columns { - &.one_lock, &.two_lock { + &.one_lock, + &.two_lock { perspective-row:first-child { --active--color: #ccc; --is_visible--cursor: normal; @@ -221,7 +222,7 @@ } &.two_lock { - perspective-row:nth-child(2) { + perspective-row:nth-child(2) { --active--color: #ccc; --is_visible--cursor: normal; } @@ -230,8 +231,8 @@ #active_columns { perspective-row:only-child { - --active--color: #ccc; - --is_visible--cursor: normal; + --active--color: #ccc; + --is_visible--cursor: normal; } } diff --git a/packages/perspective-viewer/src/less/expression_editor.less b/packages/perspective-viewer/src/less/expression_editor.less index 59ec720772..60651f239e 100644 --- a/packages/perspective-viewer/src/less/expression_editor.less +++ b/packages/perspective-viewer/src/less/expression_editor.less @@ -10,7 +10,6 @@ @import "variables"; :host { - width: 100%; .perspective-expression-editor__edit_area { @@ -126,7 +125,8 @@ .psp-expression__errored { font-weight: 700; color: var(--expression--error-color, rgb(250, 51, 51)); - text-decoration: underline dotted var(--expression--error-color, rgb(250, 51, 51)); + text-decoration: underline dotted + var(--expression--error-color, rgb(250, 51, 51)); } } -} \ No newline at end of file +} diff --git a/packages/perspective-viewer/src/less/fonts.less b/packages/perspective-viewer/src/less/fonts.less index b1c1bc9c44..8337d20909 100644 --- a/packages/perspective-viewer/src/less/fonts.less +++ b/packages/perspective-viewer/src/less/fonts.less @@ -1,3 +1,3 @@ @import (css) url("https://fonts.googleapis.com/css?family=Material+Icons"); -@import (css) url("https://fonts.googleapis.com/css?family=Open+Sans");// ~typeface-open-sans/index.css"); +@import (css) url("https://fonts.googleapis.com/css?family=Open+Sans"); // ~typeface-open-sans/index.css"); @import (css) url("https://fonts.googleapis.com/css?family=Roboto+Mono"); // ~typeface-roboto-mono/index.css"); diff --git a/packages/perspective-viewer/src/less/row.less b/packages/perspective-viewer/src/less/row.less index a3a0170900..56ce29caaa 100644 --- a/packages/perspective-viewer/src/less/row.less +++ b/packages/perspective-viewer/src/less/row.less @@ -178,7 +178,11 @@ -moz-appearance: none; -ms-appearance: none; appearance: none; - background: var(--select--background, url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSJMYXllcl8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCIKCSB2aWV3Qm94PSIwIDAgNC45IDEwIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA0LjkgMTA7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCS5zdDB7ZmlsbDojQUFBQUFBO30KPC9zdHlsZT4KPHRpdGxlPmFycm93czwvdGl0bGU+Cjxwb2x5Z29uIGNsYXNzPSJzdDAiIHBvaW50cz0iMS40LDQuNyAyLjUsMy4yIDMuNSw0LjcgIi8+Cjxwb2x5Z29uIGNsYXNzPSJzdDAiIHBvaW50cz0iMy41LDUuMyAyLjUsNi44IDEuNCw1LjMgIi8+Cjwvc3ZnPg==) no-repeat 95% 50%); + background: var( + --select--background, + url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSJMYXllcl8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCIKCSB2aWV3Qm94PSIwIDAgNC45IDEwIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA0LjkgMTA7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCS5zdDB7ZmlsbDojQUFBQUFBO30KPC9zdHlsZT4KPHRpdGxlPmFycm93czwvdGl0bGU+Cjxwb2x5Z29uIGNsYXNzPSJzdDAiIHBvaW50cz0iMS40LDQuNyAyLjUsMy4yIDMuNSw0LjcgIi8+Cjxwb2x5Z29uIGNsYXNzPSJzdDAiIHBvaW50cz0iMy41LDUuMyAyLjUsNi44IDEuNCw1LjMgIi8+Cjwvc3ZnPg==) + no-repeat 95% 50% + ); background-color: var(--select--background-color, white); color: inherit; border-radius: 5px; @@ -248,10 +252,7 @@ opacity: 0 !important; } .row_draggable { - background-color: var( - --null--background, - transparent - ) !important; + background-color: var(--null--background, transparent) !important; border-color: @border-color !important; border-width: 0 0 1px 0 !important; } diff --git a/packages/perspective-viewer/src/less/viewer.less b/packages/perspective-viewer/src/less/viewer.less index 19fa1abed4..05ce775318 100644 --- a/packages/perspective-viewer/src/less/viewer.less +++ b/packages/perspective-viewer/src/less/viewer.less @@ -169,7 +169,7 @@ flex-grow: 1; position: relative; border: var(--plugin--border, none); - overflow:hidden; + overflow: hidden; } .config { display: flex; @@ -386,8 +386,12 @@ -webkit-appearance: none; -moz-appearance: none; -ms-appearance: none; - appearance: none; - background: var(--select--background, url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSJMYXllcl8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCIKCSB2aWV3Qm94PSIwIDAgNC45IDEwIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA0LjkgMTA7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCS5zdDB7ZmlsbDojQUFBQUFBO30KPC9zdHlsZT4KPHRpdGxlPmFycm93czwvdGl0bGU+Cjxwb2x5Z29uIGNsYXNzPSJzdDAiIHBvaW50cz0iMS40LDQuNyAyLjUsMy4yIDMuNSw0LjcgIi8+Cjxwb2x5Z29uIGNsYXNzPSJzdDAiIHBvaW50cz0iMy41LDUuMyAyLjUsNi44IDEuNCw1LjMgIi8+Cjwvc3ZnPg==) no-repeat 95% 50%); + appearance: none; + background: var( + --select--background, + url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4KPHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSJMYXllcl8xIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB4PSIwcHgiIHk9IjBweCIKCSB2aWV3Qm94PSIwIDAgNC45IDEwIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCA0LjkgMTA7IiB4bWw6c3BhY2U9InByZXNlcnZlIj4KPHN0eWxlIHR5cGU9InRleHQvY3NzIj4KCS5zdDB7ZmlsbDojQUFBQUFBO30KPC9zdHlsZT4KPHRpdGxlPmFycm93czwvdGl0bGU+Cjxwb2x5Z29uIGNsYXNzPSJzdDAiIHBvaW50cz0iMS40LDQuNyAyLjUsMy4yIDMuNSw0LjcgIi8+Cjxwb2x5Z29uIGNsYXNzPSJzdDAiIHBvaW50cz0iMy41LDUuMyAyLjUsNi44IDEuNCw1LjMgIi8+Cjwvc3ZnPg==) + no-repeat 95% 50% + ); background-color: #fff; color: inherit; border-radius: 5px; @@ -461,15 +465,15 @@ } :hover::-webkit-scrollbar-thumb { - background-color: rgba(0,0,0,0.3); + background-color: rgba(0, 0, 0, 0.3); } ::-webkit-scrollbar-thumb { border-radius: 4px; - background-color: rgba(0,0,0,0); + background-color: rgba(0, 0, 0, 0); } ::-webkit-scrollbar-corner { - background-color: rgba(0,0,0,0); + background-color: rgba(0, 0, 0, 0); } } diff --git a/packages/perspective-workspace/src/less/dockpanel.less b/packages/perspective-workspace/src/less/dockpanel.less index c0f25b8e11..170202d4c6 100644 --- a/packages/perspective-workspace/src/less/dockpanel.less +++ b/packages/perspective-workspace/src/less/dockpanel.less @@ -7,7 +7,7 @@ * */ - .p-DockPanel { +.p-DockPanel { overflow: visible !important; position: absolute; background-color: var(--detail--background-color, transparent); @@ -27,25 +27,31 @@ background-color: #eee; } -.perspective-scroll-panel::-webkit-scrollbar-corner { +.perspective-scroll-panel::-webkit-scrollbar-corner { background-color: #eee; } .perspective-scroll-panel::-webkit-scrollbar-thumb { - background-color: #aaa; - //outline: 1px solid #666; - height: 0.8em; - width: 0.8em; - border: 8px solid #eee; - border-left-width: 0px; - border-top-width: 0px; + background-color: #aaa; + //outline: 1px solid #666; + height: 0.8em; + width: 0.8em; + border: 8px solid #eee; + border-left-width: 0px; + border-top-width: 0px; } -.p-DockPanel.ew, .p-DockPanel.ew .p-Widget,.p-SplitPanel.ew, .p-SplitPanel.ew .p-Widget { +.p-DockPanel.ew, +.p-DockPanel.ew .p-Widget, +.p-SplitPanel.ew, +.p-SplitPanel.ew .p-Widget { cursor: ew-resize !important; } -.p-DockPanel.ns, .p-DockPanel.ns .p-Widget, .p-SplitPanel.ns, .p-SplitPanel.ns .p-Widget { +.p-DockPanel.ns, +.p-DockPanel.ns .p-Widget, +.p-SplitPanel.ns, +.p-SplitPanel.ns .p-Widget { cursor: ns-resize !important; } @@ -53,16 +59,17 @@ min-width: 300px; } -.p-DockPanel.resizing ::slotted(perspective-viewer), .p-SplitPanel.resizing ::slotted(perspective-viewer) { +.p-DockPanel.resizing ::slotted(perspective-viewer), +.p-SplitPanel.resizing ::slotted(perspective-viewer) { pointer-events: none; } .widget-blur ::slotted(perspective-viewer) { opacity: 0.5; } - + .p-DockPanel-handle.resizing { - background-color: rgba(0,0,0,0.05); + background-color: rgba(0, 0, 0, 0.05); } .perspective-scroll-panel { @@ -71,10 +78,8 @@ .p-Panel { min-height: 100%; - } - .p-DockPanel-handle { background-color: none; } @@ -86,4 +91,4 @@ transition-duration: 50ms; transition-timing-function: ease; margin: 0px; -} \ No newline at end of file +} diff --git a/packages/perspective-workspace/src/less/injected.less b/packages/perspective-workspace/src/less/injected.less index 1d9f4bd7ad..48f888365f 100644 --- a/packages/perspective-workspace/src/less/injected.less +++ b/packages/perspective-workspace/src/less/injected.less @@ -1,2 +1,2 @@ @import "./viewer.less"; -@import "./menu.less"; \ No newline at end of file +@import "./menu.less"; diff --git a/packages/perspective-workspace/src/less/menu.less b/packages/perspective-workspace/src/less/menu.less index e5c2cd3d9f..2c0a766178 100644 --- a/packages/perspective-workspace/src/less/menu.less +++ b/packages/perspective-workspace/src/less/menu.less @@ -18,16 +18,14 @@ border-top-width: 0px; border-right-width: 1px; font: 12px Helvetica, Arial, sans-serif; - box-shadow: - rgba(0, 0, 0, 0.2) 0px 3px 1px -2px, - rgba(0, 0, 0, 0.14) 0px 2px 2px 0px, - rgba(0, 0, 0, 0.12) 0px 1px 5px 0px; + box-shadow: rgba(0, 0, 0, 0.2) 0px 3px 1px -2px, + rgba(0, 0, 0, 0.14) 0px 2px 2px 0px, rgba(0, 0, 0, 0.12) 0px 1px 5px 0px; } .p-Menu.workspace-master-menu { background: rgb(42, 47, 54); color: #ccc; - border: 1px solid rgb(85, 94, 107);; + border: 1px solid rgb(85, 94, 107); } .p-Menu-item.p-mod-active { @@ -60,12 +58,12 @@ padding: 4px 0px; } -.p-Menu-item[data-type="separator"]>div { +.p-Menu-item[data-type="separator"] > div { padding: 0; height: 9px; } -.p-Menu-item[data-type="separator"]>div::after { +.p-Menu-item[data-type="separator"] > div::after { content: ""; display: block; position: relative; @@ -98,7 +96,6 @@ background: white; border-left: 1px solid #c0c0c0; border-right: 1px solid #c0c0c0; - } .p-Menu-itemIcon:before { @@ -106,6 +103,6 @@ font-family: "Material Icons"; } -.p-mod-drag-image.p-TabBar-tab{ +.p-mod-drag-image.p-TabBar-tab { display: none; } diff --git a/packages/perspective-workspace/src/less/tabbar.less b/packages/perspective-workspace/src/less/tabbar.less index ddb8dd06a9..f9d3e48330 100644 --- a/packages/perspective-workspace/src/less/tabbar.less +++ b/packages/perspective-workspace/src/less/tabbar.less @@ -9,19 +9,18 @@ @import "~@finos/perspective-viewer/src/less/variables.less"; -.p-TabBar-tabLabel{ +.p-TabBar-tabLabel { background-color: transparent; border: none; color: #666; cursor: pointer; } - -.p-TabBar-tabLabel[value='[untitled]']{ - color: #ddd !important +.p-TabBar-tabLabel[value="[untitled]"] { + color: #ddd !important; } -.p-TabBar-tabLabel:focus{ +.p-TabBar-tabLabel:focus { outline: none; color: #666 !important; cursor: text; @@ -43,11 +42,10 @@ font-family: var(--settings--font-family, "Arial"); } -.p-TabBar-tab.p-mod-current.linked > .p-TabBar-tabConfigIcon:before{ - color: #22a0ce;; +.p-TabBar-tab.p-mod-current.linked > .p-TabBar-tabConfigIcon:before { + color: #22a0ce; } - .p-TabBar-tab > .p-TabBar-tabConfigIcon { opacity: 0; pointer-events: none; @@ -59,11 +57,13 @@ } .p-mod-current { - .p-TabBar-tabConfigIcon, .p-TabBar-tabCloseIcon { - color: #CCC; + .p-TabBar-tabConfigIcon, + .p-TabBar-tabCloseIcon { + color: #ccc; transition: color 0.2s ease-out; } - .p-TabBar-tabConfigIcon:hover, .p-TabBar-tabCloseIcon:hover { + .p-TabBar-tabConfigIcon:hover, + .p-TabBar-tabCloseIcon:hover { color: #1a7da1; transition: color 0.2s ease-out; } @@ -93,7 +93,6 @@ content: "[untitled]"; } - .divider { left: 14px; bottom: 0; @@ -129,11 +128,9 @@ min-height: 24px !important; } - @border-color: 1px solid #eaeaea; @night-border-color: 1px solid #ddd; - .pfm-button-base { background: white !important; } @@ -171,10 +168,8 @@ border-color: #eaeaea; } - - - .p-TabBar-content > .p-TabBar-tab, - .p-TabBar-content > .p-TabBar-tab.p-mod-current.settings_open { +.p-TabBar-content > .p-TabBar-tab, +.p-TabBar-content > .p-TabBar-tab.p-mod-current.settings_open { background-color: #eee; } @@ -182,8 +177,7 @@ background-color: #ddd; } - .condensed .p-TabBar-content { - +.condensed .p-TabBar-content { & > .p-TabBar-tab .p-TabBar-tabConfigIcon { display: none; } @@ -199,8 +193,7 @@ } } - - .p-TabBar-content > .p-TabBar-tab.p-mod-current.settings_open { +.p-TabBar-content > .p-TabBar-tab.p-mod-current.settings_open { border-width: 1px !important; border-color: transparent; .p-TabBar-tabToolbar { @@ -213,17 +206,16 @@ right: 10px; } &:last-child:first-child .divider { - background: none; - transition: none; - } + background: none; + transition: none; + } } - .p-TabBar-content > .p-TabBar-tab.p-mod-current.perspective_updating { +.p-TabBar-content > .p-TabBar-tab.p-mod-current.perspective_updating { .p-TabBar-tabConfigIcon { display: none; } - .p-TabBar-tabLoadingIcon { display: block; } @@ -233,7 +225,7 @@ background-color: transparent !important; } - .p-TabBar-content > .p-TabBar-tab { +.p-TabBar-content > .p-TabBar-tab { max-width: 100000px !important; flex: 0 1 100000px !important; background: none; @@ -248,12 +240,12 @@ transition: color 0.2s ease-out; } - .p-TabBar-content > .p-TabBar-tab { +.p-TabBar-content > .p-TabBar-tab { color: #ccc; } .p-TabBar-content > .p-TabBar-tab { - color: #CCC; + color: #ccc; } .p-TabBar-content > .p-TabBar-tab .p-TabBar-tabLabel { @@ -264,15 +256,15 @@ color: #ccc; } - .p-TabBar-content > .p-TabBar-tab.p-mod-current { +.p-TabBar-content > .p-TabBar-tab.p-mod-current { color: #666; } - .p-TabBar-tab.p-mod-current .p-TabBar-tabLabel { +.p-TabBar-tab.p-mod-current .p-TabBar-tabLabel { white-space: nowrap !important; } - .p-TabBar-tabLabel { +.p-TabBar-tabLabel { font-family: "Open Sans"; font-weight: 400; font-size: 12px; @@ -282,7 +274,6 @@ word-break: break-all; } - .p-TabBar-tabCloseIcon, .p-TabBar-tabConfigIcon { cursor: pointer !important; @@ -299,13 +290,12 @@ padding-right: 12px; } - .bottom .p-TabBar-tab { flex-basis: 10000px !important; max-width: 10000px !important; } - .p-TabBar-content > .p-TabBar-tab.p-mod-current { +.p-TabBar-content > .p-TabBar-tab.p-mod-current { color: var(--detail-tabbar--color, #666) !important; border: var(--detail-tabbar--border, @border-color); background-color: var(--detail-tabbar--background-color, white); @@ -321,13 +311,11 @@ background-color: #ccc; } - -.perspective-workspace.context-menu * .p-TabBar.context-focus{ +.perspective-workspace.context-menu * .p-TabBar.context-focus { opacity: 1; } -.perspective-workspace.context-menu * .p-TabBar{ +.perspective-workspace.context-menu * .p-TabBar { opacity: 0.2; transition: opacity 0.2s ease-out; } - diff --git a/packages/perspective-workspace/src/less/viewer.less b/packages/perspective-workspace/src/less/viewer.less index 7c64435706..248a4a0132 100644 --- a/packages/perspective-workspace/src/less/viewer.less +++ b/packages/perspective-workspace/src/less/viewer.less @@ -1,53 +1,63 @@ @border-color: 1px solid #eaeaea; .workspace-master-widget { - --settings-button--content: var(--open-settings-button--content, "\1F527") !important; + --settings-button--content: var( + --open-settings-button--content, + "\1F527" + ) !important; } .workspace-master-widget[settings] { - --settings-button--content: var(--close-settings-button--content, "\1F527") !important;; + --settings-button--content: var( + --close-settings-button--content, + "\1F527" + ) !important; } .workspace-detail-widget.widget-maximize { - --settings-button--content: var(--open-settings-button--content, "\1F527") !important; + --settings-button--content: var( + --open-settings-button--content, + "\1F527" + ) !important; } .workspace-detail-widget.widget-maximize[settings] { - --settings-button--content: var(--close-settings-button--content, "\1F527") !important; + --settings-button--content: var( + --close-settings-button--content, + "\1F527" + ) !important; } - .p-DockPanel-widget { - border: @border-color; - border-width: 0px !important; - min-width: 300px; - min-height: 200px; + border: @border-color; + border-width: 0px !important; + min-width: 300px; + min-height: 200px; } .workspace-detail-widget { - --settings-button--content: "" !important; - --plugin--border: 1px solid #eaeaea; + --settings-button--content: "" !important; + --plugin--border: 1px solid #eaeaea; } -.context-menu > perspective-viewer.context-focus{ - opacity: 1; - +.context-menu > perspective-viewer.context-focus { + opacity: 1; } .context-menu > perspective-viewer { - opacity: 0.2; + opacity: 0.2; } perspective-viewer { - flex: 1; - position: relative; - display: block; - align-items: center; - justify-content: center; - flex-direction: column; - width: 100%; - height: 100%; - overflow: visible !important; - font-family: "Open Sans", Arial, sans-serif; - transition: opacity 0.2s ease-out; - } + flex: 1; + position: relative; + display: block; + align-items: center; + justify-content: center; + flex-direction: column; + width: 100%; + height: 100%; + overflow: visible !important; + font-family: "Open Sans", Arial, sans-serif; + transition: opacity 0.2s ease-out; +} diff --git a/packages/perspective-workspace/src/less/widget.less b/packages/perspective-workspace/src/less/widget.less index 6e5e995a61..eec9e993ed 100644 --- a/packages/perspective-workspace/src/less/widget.less +++ b/packages/perspective-workspace/src/less/widget.less @@ -9,17 +9,17 @@ @border-color: 1px solid #eaeaea; -.viewer-container{ - flex: 1; - height: 100%; - overflow: hidden; +.viewer-container { + flex: 1; + height: 100%; + overflow: hidden; } - -.workspace-widget{ + +.workspace-widget { display: flex; flex-direction: column; border: @border-color; border-width: 0px !important; min-width: 300px; min-height: 200px; -} \ No newline at end of file +} diff --git a/packages/perspective-workspace/src/less/workspace.less b/packages/perspective-workspace/src/less/workspace.less index f238f9e722..8d3911ced5 100644 --- a/packages/perspective-workspace/src/less/workspace.less +++ b/packages/perspective-workspace/src/less/workspace.less @@ -6,7 +6,7 @@ * the Apache License 2.0. The full license can be found in the LICENSE file. * */ - + :host { @import "~@lumino/widgets/style/index.css"; @import "./tabbar.less"; @@ -18,29 +18,28 @@ width: 100%; height: 100%; - .workspace{ + .workspace { width: 100%; height: 100%; } - .p-SplitPanel{ + .p-SplitPanel { height: 100%; width: 100%; } - + div.p-SplitPanel-handle { - background-color: var(--master-divider--background-color); + background-color: var(--master-divider--background-color); } .master-panel { background-color: rgb(47, 49, 54); } - .perspective-workspace.context-menu * .p-SplitPanel-handle{ + .perspective-workspace.context-menu * .p-SplitPanel-handle { opacity: 0.2; } - .perspective-workspace.context-menu > .p-SplitPanel-handle{ + .perspective-workspace.context-menu > .p-SplitPanel-handle { opacity: 0.2; } - } diff --git a/packages/perspective/src/config/common.config.js b/packages/perspective/src/config/common.config.js index 9f00709627..cfa3cfbceb 100644 --- a/packages/perspective/src/config/common.config.js +++ b/packages/perspective/src/config/common.config.js @@ -1,76 +1,76 @@ -const webpack = require("webpack"); -const path = require("path"); -const PerspectivePlugin = require("@finos/perspective-webpack-plugin"); -const TerserPlugin = require("terser-webpack-plugin"); -const plugins = [new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /(en|es|fr)$/)]; - -function common({build_worker, no_minify, inline} = {}) { - plugins.push(new PerspectivePlugin({build_worker: build_worker, workerLoaderOptions: {inline, name: "[name].worker.js"}, wasmLoaderOptions: {inline, name: "[name]"}})); - return { - mode: process.env.PSP_NO_MINIFY || process.env.PSP_DEBUG || no_minify ? "development" : process.env.NODE_ENV || "production", - plugins: plugins, - module: { - rules: [ - { - test: /\.less$/, - exclude: /node_modules\/(?!regular-table)/, - use: [{loader: "css-loader"}, {loader: "clean-css-loader", options: {level: 2, skipWarn: true}}, {loader: "less-loader"}] - }, - { - test: /\.(html)$/, - use: { - loader: "html-loader", - options: {} - } - }, - { - test: /\.(arrow)$/, - use: { - loader: "arraybuffer-loader", - options: {} - } - } - ] - }, - devtool: "source-map", - node: { - fs: "empty", - Buffer: false - }, - performance: { - hints: false, - maxEntrypointSize: 512000, - maxAssetSize: 512000 - }, - stats: {modules: false, hash: false, version: false, builtAt: false, entrypoints: false}, - optimization: { - minimizer: [ - new TerserPlugin({ - terserOptions: { - output: { - ascii_only: true - }, - keep_infinity: true - }, - cache: true, - parallel: true, - test: /\.js(\?.*)?$/i, - exclude: /(node|wasm|asmjs)/, - sourceMap: true - }) - ] - } - }; -} - -// Remove absolute paths from webpack source-maps - -const ABS_PATH = path.resolve(__dirname, "..", "..", "..", ".."); -const devtoolModuleFilenameTemplate = info => `webpack:///${path.relative(ABS_PATH, info.absoluteResourcePath)}`; - -module.exports = (options, f) => { - let new_config = Object.assign({}, common(options)); - new_config = f(new_config); - new_config.output.devtoolModuleFilenameTemplate = devtoolModuleFilenameTemplate; - return new_config; -}; +const webpack = require("webpack"); +const path = require("path"); +const PerspectivePlugin = require("@finos/perspective-webpack-plugin"); +const TerserPlugin = require("terser-webpack-plugin"); +const plugins = [new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /(en|es|fr)$/)]; + +function common({build_worker, no_minify, inline} = {}) { + plugins.push(new PerspectivePlugin({build_worker: build_worker, workerLoaderOptions: {inline, name: "[name].worker.js"}, wasmLoaderOptions: {inline, name: "[name]"}})); + return { + mode: process.env.PSP_NO_MINIFY || process.env.PSP_DEBUG || no_minify ? "development" : process.env.NODE_ENV || "production", + plugins: plugins, + module: { + rules: [ + { + test: /\.less$/, + exclude: /node_modules\/(?!regular-table)/, + use: [{loader: "css-loader"}, {loader: "clean-css-loader", options: {level: 2, skipWarn: true}}, {loader: "less-loader"}] + }, + { + test: /\.(html)$/, + use: { + loader: "html-loader", + options: {} + } + }, + { + test: /\.(arrow)$/, + use: { + loader: "arraybuffer-loader", + options: {} + } + } + ] + }, + devtool: "source-map", + node: { + fs: "empty", + Buffer: false + }, + performance: { + hints: false, + maxEntrypointSize: 512000, + maxAssetSize: 512000 + }, + stats: {modules: false, hash: false, version: false, builtAt: false, entrypoints: false}, + optimization: { + minimizer: [ + new TerserPlugin({ + terserOptions: { + output: { + ascii_only: true + }, + keep_infinity: true + }, + cache: true, + parallel: true, + test: /\.js(\?.*)?$/i, + exclude: /(node|wasm|asmjs)/, + sourceMap: true + }) + ] + } + }; +} + +// Remove absolute paths from webpack source-maps + +const ABS_PATH = path.resolve(__dirname, "..", "..", "..", ".."); +const devtoolModuleFilenameTemplate = info => `webpack:///${path.relative(ABS_PATH, info.absoluteResourcePath)}`; + +module.exports = (options, f) => { + let new_config = Object.assign({}, common(options)); + new_config = f(new_config); + new_config.output.devtoolModuleFilenameTemplate = devtoolModuleFilenameTemplate; + return new_config; +}; diff --git a/packages/perspective/src/js/perspective.node.js b/packages/perspective/src/js/perspective.node.js index 67f9d51421..c84e68bbdc 100644 --- a/packages/perspective/src/js/perspective.node.js +++ b/packages/perspective/src/js/perspective.node.js @@ -1,188 +1,188 @@ -/****************************************************************************** - * - * 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. - * - */ - -const {Client} = require("./api/client.js"); -const {Server} = require("./api/server.js"); -const {WebSocketManager, WebSocketClient} = require("./websocket"); - -const perspective = require("./perspective.js").default; - -const fs = require("fs"); -const http = require("http"); -const WebSocket = require("ws"); -const process = require("process"); - -const path = require("path"); - -const load_perspective = require("./psp.async.js").default; - -// eslint-disable-next-line no-undef - -const LOCAL_PATH = path.join(process.cwd(), "node_modules"); -const buffer = require("./psp.async.wasm.js").default; - -const SYNC_SERVER = new (class extends Server { - init(msg) { - load_perspective({ - wasmBinary: buffer, - wasmJSMethod: "native-wasm" - }).then(core => { - this.perspective = perspective(core); - super.init(msg); - }); - } - - post(msg) { - SYNC_CLIENT._handle({data: msg}); - } -})(); - -const SYNC_CLIENT = new (class extends Client { - send(msg) { - SYNC_SERVER.process(msg); - } -})(); - -SYNC_CLIENT.send({id: -1, cmd: "init"}); - -module.exports = SYNC_CLIENT; -module.exports.sync_module = () => SYNC_SERVER.perspective; - -const DEFAULT_ASSETS = [ - "@finos/perspective/dist/umd", - "@finos/perspective-bench/dist", - "@finos/perspective-viewer/dist/umd", - "@finos/perspective-viewer-highcharts/dist/umd", - "@finos/perspective-viewer-hypergrid/dist/umd", - "@finos/perspective-viewer-datagrid/dist/umd", - "@finos/perspective-viewer-d3fc/dist/umd", - "@finos/perspective-workspace/dist/umd" -]; - -const CONTENT_TYPES = { - ".js": "text/javascript", - ".css": "text/css", - ".json": "application/json", - ".arrow": "arraybuffer", - ".wasm": "application/wasm" -}; - -function read_promise(filePath) { - return new Promise((resolve, reject) => { - fs.readFile(filePath, function(error, content) { - if (error && error.code !== "ENOENT") { - reject(error); - } else { - resolve(content); - } - }); - }); -} - -/** - * Host a Perspective server that hosts data, code files, etc. - */ -function perspective_assets(assets, host_psp) { - return async function(request, response) { - response.setHeader("Access-Control-Allow-Origin", "*"); - response.setHeader("Access-Control-Request-Method", "*"); - response.setHeader("Access-Control-Allow-Methods", "OPTIONS,GET"); - response.setHeader("Access-Control-Allow-Headers", "*"); - let url = request.url.split(/[\?\#]/)[0]; - if (url === "/") { - url = "/index.html"; - } - let extname = path.extname(url); - let contentType = CONTENT_TYPES[extname] || "text/html"; - try { - for (let rootDir of assets) { - let filePath = rootDir + url; - let content = await read_promise(filePath); - if (typeof content !== "undefined") { - console.log(`200 ${url}`); - response.writeHead(200, {"Content-Type": contentType}); - response.end(content, extname === ".arrow" ? "user-defined" : "utf-8"); - return; - } - } - if (host_psp || typeof host_psp === "undefined") { - for (let rootDir of DEFAULT_ASSETS) { - try { - let paths = require.resolve.paths(rootDir + url); - paths = [...paths, ...assets.map(x => path.join(x, "node_modules")), LOCAL_PATH]; - let filePath = require.resolve(rootDir + url, {paths}); - let content = await read_promise(filePath); - if (typeof content !== "undefined") { - console.log(`200 ${url}`); - response.writeHead(200, {"Content-Type": contentType}); - response.end(content, extname === ".arrow" ? "user-defined" : "utf-8"); - return; - } - } catch (e) {} - } - } - if (url.indexOf("favicon.ico") > -1) { - response.writeHead(200); - response.end("", "utf-8"); - } else { - console.error(`404 ${url}`); - response.writeHead(404); - response.end("", "utf-8"); - } - } catch (error) { - if (error.code !== "ENOENT") { - console.error(`500 ${url}`); - response.writeHead(500); - response.end("", "utf-8"); - } - } - }; -} - -class WebSocketServer extends WebSocketManager { - constructor({assets, host_psp, port, on_start} = {}) { - super(); - port = typeof port === "undefined" ? 8080 : port; - assets = assets || ["./"]; - - // Serve Perspective files through HTTP - this._server = http.createServer(perspective_assets(assets, host_psp)); - - // Serve Worker API through WebSockets - this._wss = new WebSocket.Server({noServer: true, perMessageDeflate: true}); - - // When the server starts, define how to handle messages - this._wss.on("connection", ws => this.add_connection(ws)); - - this._server.on("upgrade", (request, socket, head) => { - console.log("200 *** websocket upgrade ***"); - this._wss.handleUpgrade(request, socket, head, sock => this._wss.emit("connection", sock, request)); - }); - - this._server.listen(port, () => { - console.log(`Listening on port ${this._server.address().port}`); - if (on_start) { - on_start(); - } - }); - } - - close() { - this._server.close(); - } -} - -const websocket = url => { - return new WebSocketClient(new WebSocket(url)); -}; - -module.exports.websocket = websocket; -module.exports.perspective_assets = perspective_assets; -module.exports.WebSocketServer = WebSocketServer; -module.exports.WebSocketManager = WebSocketManager; +/****************************************************************************** + * + * 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. + * + */ + +const {Client} = require("./api/client.js"); +const {Server} = require("./api/server.js"); +const {WebSocketManager, WebSocketClient} = require("./websocket"); + +const perspective = require("./perspective.js").default; + +const fs = require("fs"); +const http = require("http"); +const WebSocket = require("ws"); +const process = require("process"); + +const path = require("path"); + +const load_perspective = require("./psp.async.js").default; + +// eslint-disable-next-line no-undef + +const LOCAL_PATH = path.join(process.cwd(), "node_modules"); +const buffer = require("./psp.async.wasm.js").default; + +const SYNC_SERVER = new (class extends Server { + init(msg) { + load_perspective({ + wasmBinary: buffer, + wasmJSMethod: "native-wasm" + }).then(core => { + this.perspective = perspective(core); + super.init(msg); + }); + } + + post(msg) { + SYNC_CLIENT._handle({data: msg}); + } +})(); + +const SYNC_CLIENT = new (class extends Client { + send(msg) { + SYNC_SERVER.process(msg); + } +})(); + +SYNC_CLIENT.send({id: -1, cmd: "init"}); + +module.exports = SYNC_CLIENT; +module.exports.sync_module = () => SYNC_SERVER.perspective; + +const DEFAULT_ASSETS = [ + "@finos/perspective/dist/umd", + "@finos/perspective-bench/dist", + "@finos/perspective-viewer/dist/umd", + "@finos/perspective-viewer-highcharts/dist/umd", + "@finos/perspective-viewer-hypergrid/dist/umd", + "@finos/perspective-viewer-datagrid/dist/umd", + "@finos/perspective-viewer-d3fc/dist/umd", + "@finos/perspective-workspace/dist/umd" +]; + +const CONTENT_TYPES = { + ".js": "text/javascript", + ".css": "text/css", + ".json": "application/json", + ".arrow": "arraybuffer", + ".wasm": "application/wasm" +}; + +function read_promise(filePath) { + return new Promise((resolve, reject) => { + fs.readFile(filePath, function(error, content) { + if (error && error.code !== "ENOENT") { + reject(error); + } else { + resolve(content); + } + }); + }); +} + +/** + * Host a Perspective server that hosts data, code files, etc. + */ +function perspective_assets(assets, host_psp) { + return async function(request, response) { + response.setHeader("Access-Control-Allow-Origin", "*"); + response.setHeader("Access-Control-Request-Method", "*"); + response.setHeader("Access-Control-Allow-Methods", "OPTIONS,GET"); + response.setHeader("Access-Control-Allow-Headers", "*"); + let url = request.url.split(/[\?\#]/)[0]; + if (url === "/") { + url = "/index.html"; + } + let extname = path.extname(url); + let contentType = CONTENT_TYPES[extname] || "text/html"; + try { + for (let rootDir of assets) { + let filePath = rootDir + url; + let content = await read_promise(filePath); + if (typeof content !== "undefined") { + console.log(`200 ${url}`); + response.writeHead(200, {"Content-Type": contentType}); + response.end(content, extname === ".arrow" ? "user-defined" : "utf-8"); + return; + } + } + if (host_psp || typeof host_psp === "undefined") { + for (let rootDir of DEFAULT_ASSETS) { + try { + let paths = require.resolve.paths(rootDir + url); + paths = [...paths, ...assets.map(x => path.join(x, "node_modules")), LOCAL_PATH]; + let filePath = require.resolve(rootDir + url, {paths}); + let content = await read_promise(filePath); + if (typeof content !== "undefined") { + console.log(`200 ${url}`); + response.writeHead(200, {"Content-Type": contentType}); + response.end(content, extname === ".arrow" ? "user-defined" : "utf-8"); + return; + } + } catch (e) {} + } + } + if (url.indexOf("favicon.ico") > -1) { + response.writeHead(200); + response.end("", "utf-8"); + } else { + console.error(`404 ${url}`); + response.writeHead(404); + response.end("", "utf-8"); + } + } catch (error) { + if (error.code !== "ENOENT") { + console.error(`500 ${url}`); + response.writeHead(500); + response.end("", "utf-8"); + } + } + }; +} + +class WebSocketServer extends WebSocketManager { + constructor({assets, host_psp, port, on_start} = {}) { + super(); + port = typeof port === "undefined" ? 8080 : port; + assets = assets || ["./"]; + + // Serve Perspective files through HTTP + this._server = http.createServer(perspective_assets(assets, host_psp)); + + // Serve Worker API through WebSockets + this._wss = new WebSocket.Server({noServer: true, perMessageDeflate: true}); + + // When the server starts, define how to handle messages + this._wss.on("connection", ws => this.add_connection(ws)); + + this._server.on("upgrade", (request, socket, head) => { + console.log("200 *** websocket upgrade ***"); + this._wss.handleUpgrade(request, socket, head, sock => this._wss.emit("connection", sock, request)); + }); + + this._server.listen(port, () => { + console.log(`Listening on port ${this._server.address().port}`); + if (on_start) { + on_start(); + } + }); + } + + close() { + this._server.close(); + } +} + +const websocket = url => { + return new WebSocketClient(new WebSocket(url)); +}; + +module.exports.websocket = websocket; +module.exports.perspective_assets = perspective_assets; +module.exports.WebSocketServer = WebSocketServer; +module.exports.WebSocketManager = WebSocketManager; diff --git a/packages/perspective/src/js/perspective.parallel.js b/packages/perspective/src/js/perspective.parallel.js index 1e6ea6f739..8779e8431b 100644 --- a/packages/perspective/src/js/perspective.parallel.js +++ b/packages/perspective/src/js/perspective.parallel.js @@ -1,184 +1,184 @@ -/****************************************************************************** - * - * 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 defaults from "./config/constants.js"; -import {get_config} from "./config"; -import {Client} from "./api/client.js"; -const {WebSocketClient} = require("./websocket"); - -import wasm_worker from "./perspective.wasm.js"; -import wasm from "./psp.async.wasm.js"; -import {override_config} from "../../dist/esm/config/index.js"; - -// eslint-disable-next-line max-len -const INLINE_WARNING = `Perspective has been compiled in INLINE mode. While Perspective's runtime performance is not affected, you may see smaller assets size and faster engine initial load time using "@finos/perspective-webpack-plugin" to build your application. - -https://perspective.finos.org/docs/md/installation.html#-an-important-note-about-hosting`; - -/** - * Singleton WASM file download cache. - */ -const override = new (class { - _fetch(url) { - return new Promise(resolve => { - let wasmXHR = new XMLHttpRequest(); - wasmXHR.open("GET", url, true); - wasmXHR.responseType = "arraybuffer"; - wasmXHR.onload = () => { - resolve(wasmXHR.response); - }; - wasmXHR.send(null); - }); - } - - worker() { - return wasm_worker(); - } - - async wasm() { - if (wasm instanceof ArrayBuffer) { - console.warn(INLINE_WARNING); - this._wasm = wasm; - } else { - this._wasm = await this._fetch(wasm); - } - return this._wasm; - } -})(); - -/** - * WebWorker extends Perspective's `worker` class and defines interactions using - * the WebWorker API. - * - * This class serves as the client API for transporting messages to/from Web - * Workers. - */ -class WebWorkerClient extends Client { - constructor(config) { - if (config) { - override_config(config); - } - super(); - this.register(); - } - - /** - * When the worker is created, load either the ASM or WASM bundle depending - * on WebAssembly compatibility. Don't use transferrable so multiple - * workers can be instantiated. - */ - async register() { - let _worker; - const msg = {cmd: "init", config: get_config()}; - if (typeof WebAssembly === "undefined") { - throw new Error("WebAssembly not supported. Support for ASM.JS has been removed as of 0.3.1."); - } else { - [_worker, msg.buffer] = await Promise.all([override.worker(), override.wasm()]); - } - for (var key in this._worker) { - _worker[key] = this._worker[key]; - } - this._worker = _worker; - this._worker.addEventListener("message", this._handle.bind(this)); - this._worker.postMessage(msg); - this._detect_transferable(); - } - - /** - * Send a message from the worker, using transferables if necessary. - * - * @param {*} msg - */ - send(msg) { - if (this._worker.transferable && msg.args && msg.args[0] instanceof ArrayBuffer) { - this._worker.postMessage(msg, msg.args[0]); - } else { - this._worker.postMessage(msg); - } - } - - terminate() { - this._worker.terminate(); - this._worker = undefined; - } - - _detect_transferable() { - var ab = new ArrayBuffer(1); - this._worker.postMessage(ab, [ab]); - this._worker.transferable = ab.byteLength === 0; - if (!this._worker.transferable) { - console.warn("Transferable support not detected"); - } else { - console.log("Transferable support detected"); - } - } -} - -/****************************************************************************** - * - * Web Worker Singleton - * - */ - -const WORKER_SINGLETON = (function() { - let __WORKER__, __CONFIG__; - return { - getInstance: function(config) { - if (__WORKER__ === undefined) { - __WORKER__ = new WebWorkerClient(config); - } - const config_str = JSON.stringify(config); - if (__CONFIG__ && config_str !== __CONFIG__) { - throw new Error(`Confiuration object for shared_worker() has changed - this is probably a bug in your application.`); - } - __CONFIG__ = config_str; - return __WORKER__; - } - }; -})(); - -/** - * If Perspective is loaded with the `preload` attribute, pre-initialize the - * worker so it is available at page render. - */ -if (document.currentScript && document.currentScript.hasAttribute("preload")) { - WORKER_SINGLETON.getInstance(); -} - -const mod = { - override: x => override.set(x), - - /** - * Create a new WebWorkerClient instance. s - * @param {*} [config] An optional perspective config object override - */ - worker(config) { - return new WebWorkerClient(config); - }, - - /** - * Create a new WebSocketClient instance. The `url` parameter is provided, - * load the worker at `url` using a WebSocket. s - * @param {*} url Defaults to `window.location.origin` - * @param {*} [config] An optional perspective config object override - */ - websocket(url = window.location.origin.replace("http", "ws")) { - return new WebSocketClient(new WebSocket(url)); - }, - - shared_worker(config) { - return WORKER_SINGLETON.getInstance(config); - } -}; - -for (let prop of Object.keys(defaults)) { - mod[prop] = defaults[prop]; -} - -export default mod; +/****************************************************************************** + * + * 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 defaults from "./config/constants.js"; +import {get_config} from "./config"; +import {Client} from "./api/client.js"; +const {WebSocketClient} = require("./websocket"); + +import wasm_worker from "./perspective.wasm.js"; +import wasm from "./psp.async.wasm.js"; +import {override_config} from "../../dist/esm/config/index.js"; + +// eslint-disable-next-line max-len +const INLINE_WARNING = `Perspective has been compiled in INLINE mode. While Perspective's runtime performance is not affected, you may see smaller assets size and faster engine initial load time using "@finos/perspective-webpack-plugin" to build your application. + +https://perspective.finos.org/docs/md/installation.html#-an-important-note-about-hosting`; + +/** + * Singleton WASM file download cache. + */ +const override = new (class { + _fetch(url) { + return new Promise(resolve => { + let wasmXHR = new XMLHttpRequest(); + wasmXHR.open("GET", url, true); + wasmXHR.responseType = "arraybuffer"; + wasmXHR.onload = () => { + resolve(wasmXHR.response); + }; + wasmXHR.send(null); + }); + } + + worker() { + return wasm_worker(); + } + + async wasm() { + if (wasm instanceof ArrayBuffer) { + console.warn(INLINE_WARNING); + this._wasm = wasm; + } else { + this._wasm = await this._fetch(wasm); + } + return this._wasm; + } +})(); + +/** + * WebWorker extends Perspective's `worker` class and defines interactions using + * the WebWorker API. + * + * This class serves as the client API for transporting messages to/from Web + * Workers. + */ +class WebWorkerClient extends Client { + constructor(config) { + if (config) { + override_config(config); + } + super(); + this.register(); + } + + /** + * When the worker is created, load either the ASM or WASM bundle depending + * on WebAssembly compatibility. Don't use transferrable so multiple + * workers can be instantiated. + */ + async register() { + let _worker; + const msg = {cmd: "init", config: get_config()}; + if (typeof WebAssembly === "undefined") { + throw new Error("WebAssembly not supported. Support for ASM.JS has been removed as of 0.3.1."); + } else { + [_worker, msg.buffer] = await Promise.all([override.worker(), override.wasm()]); + } + for (var key in this._worker) { + _worker[key] = this._worker[key]; + } + this._worker = _worker; + this._worker.addEventListener("message", this._handle.bind(this)); + this._worker.postMessage(msg); + this._detect_transferable(); + } + + /** + * Send a message from the worker, using transferables if necessary. + * + * @param {*} msg + */ + send(msg) { + if (this._worker.transferable && msg.args && msg.args[0] instanceof ArrayBuffer) { + this._worker.postMessage(msg, msg.args[0]); + } else { + this._worker.postMessage(msg); + } + } + + terminate() { + this._worker.terminate(); + this._worker = undefined; + } + + _detect_transferable() { + var ab = new ArrayBuffer(1); + this._worker.postMessage(ab, [ab]); + this._worker.transferable = ab.byteLength === 0; + if (!this._worker.transferable) { + console.warn("Transferable support not detected"); + } else { + console.log("Transferable support detected"); + } + } +} + +/****************************************************************************** + * + * Web Worker Singleton + * + */ + +const WORKER_SINGLETON = (function() { + let __WORKER__, __CONFIG__; + return { + getInstance: function(config) { + if (__WORKER__ === undefined) { + __WORKER__ = new WebWorkerClient(config); + } + const config_str = JSON.stringify(config); + if (__CONFIG__ && config_str !== __CONFIG__) { + throw new Error(`Confiuration object for shared_worker() has changed - this is probably a bug in your application.`); + } + __CONFIG__ = config_str; + return __WORKER__; + } + }; +})(); + +/** + * If Perspective is loaded with the `preload` attribute, pre-initialize the + * worker so it is available at page render. + */ +if (document.currentScript && document.currentScript.hasAttribute("preload")) { + WORKER_SINGLETON.getInstance(); +} + +const mod = { + override: x => override.set(x), + + /** + * Create a new WebWorkerClient instance. s + * @param {*} [config] An optional perspective config object override + */ + worker(config) { + return new WebWorkerClient(config); + }, + + /** + * Create a new WebSocketClient instance. The `url` parameter is provided, + * load the worker at `url` using a WebSocket. s + * @param {*} url Defaults to `window.location.origin` + * @param {*} [config] An optional perspective config object override + */ + websocket(url = window.location.origin.replace("http", "ws")) { + return new WebSocketClient(new WebSocket(url)); + }, + + shared_worker(config) { + return WORKER_SINGLETON.getInstance(config); + } +}; + +for (let prop of Object.keys(defaults)) { + mod[prop] = defaults[prop]; +} + +export default mod; diff --git a/packages/perspective/src/js/perspective.wasm.js b/packages/perspective/src/js/perspective.wasm.js index 8dd7fb9a9c..4d4c417bde 100644 --- a/packages/perspective/src/js/perspective.wasm.js +++ b/packages/perspective/src/js/perspective.wasm.js @@ -1,27 +1,27 @@ -/****************************************************************************** - * - * 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 load_perspective from "../../dist/obj/psp.async.js"; -import perspective from "./perspective.js"; - -let _perspective_instance; - -if (global.document !== undefined && typeof WebAssembly !== "undefined") { - _perspective_instance = global.perspective = perspective( - load_perspective({ - wasmJSMethod: "native-wasm", - printErr: x => console.error(x), - print: x => console.log(x) - }) - ); -} else { - _perspective_instance = global.perspective = perspective(load_perspective); -} - -export default _perspective_instance; +/****************************************************************************** + * + * 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 load_perspective from "../../dist/obj/psp.async.js"; +import perspective from "./perspective.js"; + +let _perspective_instance; + +if (global.document !== undefined && typeof WebAssembly !== "undefined") { + _perspective_instance = global.perspective = perspective( + load_perspective({ + wasmJSMethod: "native-wasm", + printErr: x => console.error(x), + print: x => console.log(x) + }) + ); +} else { + _perspective_instance = global.perspective = perspective(load_perspective); +} + +export default _perspective_instance; diff --git a/packages/perspective/test/config/test_node.config.js b/packages/perspective/test/config/test_node.config.js index af9e473e98..88f9a12d11 100644 --- a/packages/perspective/test/config/test_node.config.js +++ b/packages/perspective/test/config/test_node.config.js @@ -1,17 +1,17 @@ -const path = require("path"); -const common = require("../../src/config/common.config.js"); - -module.exports = Object.assign({}, common({no_minify: true}), { - entry: "./test/js/perspective.spec.js", - target: "node", - externals: [/^([a-z0-9]|\@(?!apache\-arrow)).*?(?!wasm)$/g], - node: { - __dirname: false, - __filename: false - }, - output: { - filename: "perspective.spec.js", - path: path.resolve(__dirname, "../../build"), - libraryTarget: "umd" - } -}); +const path = require("path"); +const common = require("../../src/config/common.config.js"); + +module.exports = Object.assign({}, common({no_minify: true}), { + entry: "./test/js/perspective.spec.js", + target: "node", + externals: [/^([a-z0-9]|\@(?!apache\-arrow)).*?(?!wasm)$/g], + node: { + __dirname: false, + __filename: false + }, + output: { + filename: "perspective.spec.js", + path: path.resolve(__dirname, "../../build"), + libraryTarget: "umd" + } +});