Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/dual y axis POC #564

Merged
merged 10 commits into from
May 15, 2019
61 changes: 61 additions & 0 deletions packages/perspective-viewer-d3fc/src/js/axis/axisSplitter.js
Original file line number Diff line number Diff line change
@@ -0,0 +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 {splitterLabels} from "./splitterLabels";

export const axisSplitter = (settings, sourceData) => {
let color;

// 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
const data = haveSplit ? sourceData.map(d => d.filter(v => !altValue(v.key))) : sourceData;
const altData = haveSplit ? sourceData.map(d => d.filter(v => altValue(v.key))) : 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 = () => data;
splitter.altData = () => altData;

return splitter;
};
119 changes: 116 additions & 3 deletions packages/perspective-viewer-d3fc/src/js/axis/chartFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,16 @@
* 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);
export const chartCanvasFactory = (xAxis, yAxis) => chartFactory(xAxis, yAxis, fc.chartCanvasCartesian);
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 chartFactory = (xAxis, yAxis, cartesian) => {
const chart = cartesian({
xScale: xAxis.scale,
yScale: yAxis.scale,
Expand All @@ -30,11 +34,120 @@ const chartFactory = (xAxis, yAxis, cartesian) => {
.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) => {
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", "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 = fc.axisRight(y2Scale);

// 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;
};
72 changes: 72 additions & 0 deletions packages/perspective-viewer-d3fc/src/js/axis/splitterLabels.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/******************************************************************************
*
* 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;
};
28 changes: 23 additions & 5 deletions packages/perspective-viewer-d3fc/src/js/charts/line.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ 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";
Expand Down Expand Up @@ -39,14 +40,23 @@ function lineChart(container, settings) {
.excludeType(AXIS_TYPES.linear)
.settingName("crossValues")
.valueName("crossValue")(data);
const yAxis = axisFactory(settings)
const yAxisFactory = axisFactory(settings)
.settingName("mainValues")
.valueName("mainValue")
.orient("vertical")
.include([0])
.paddingStrategy(paddingStrategy)(data);
.paddingStrategy(paddingStrategy);

const chart = chartSvgFactory(xAxis, yAxis).plotArea(withGridLines(series).orient("vertical"));
// 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).orient("vertical");
const chart = chartSvgFactory(xAxis, yAxis1)
.axisSplitter(splitter)
.plotArea(plotSeries);

chart.yNice && chart.yNice();

Expand All @@ -58,12 +68,20 @@ function lineChart(container, settings) {
const toolTip = nearbyTip()
.settings(settings)
.xScale(xAxis.scale)
.yScale(yAxis.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(data).call(zoomChart);
container.datum(splitter.data()).call(zoomChart);
container.call(toolTip);
container.call(legend);
}
Expand Down
Loading