Skip to content

Commit

Permalink
Merge pull request #564 from redbearsam/feature/dual-y-axis
Browse files Browse the repository at this point in the history
Feature/dual y axis POC
  • Loading branch information
texodus authored May 15, 2019
2 parents c90ba6c + 6bf5d02 commit 302b8e8
Show file tree
Hide file tree
Showing 8 changed files with 342 additions and 23 deletions.
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

0 comments on commit 302b8e8

Please sign in to comment.