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

Added "Multi Chart" that allows mix-and-match of different series types #586

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
88 changes: 55 additions & 33 deletions packages/perspective-viewer-d3fc/src/js/axis/axisSplitter.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,43 +9,52 @@
import {splitterLabels} from "./splitterLabels";

export const axisSplitter = (settings, sourceData, splitFn = dataSplitFunction) => {
// splitMainValues is an array of main-value names to put into the alt-axis
const splitMainValues = settings.splitMainValues || [];

let color;
let data;
let altData;
let decorate = () => {};

// splitMainValues is an array of main-value names to put into the alt-axis
const splitMainValues = settings.splitMainValues || [];
const altValue = name => {
// Renderer to show the special controls for moving between axes
const splitter = selection => {
if (settings["mainValues"].length !== 1) {
const labelsInfo = settings["mainValues"].map((v, i) => ({
index: i,
name: v.name
}));
const mainLabels = labelsInfo.filter(v => !splitter.isOnAltAxis(v.name));
const altLabels = labelsInfo.filter(v => splitter.isOnAltAxis(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)
);
}

decorate(selection.select(".y-label-container"), 0);
decorate(selection.select(".y2-label-container"), 1);
};

splitter.isOnAltAxis = name => {
// Check whether this "aggregate" name should be on the alternate y-axis
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;
const haveSplit = settings["mainValues"].some(m => splitter.isOnAltAxis(m.name));
splitter.haveSplit = () => haveSplit;

// 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)
);
};
if (sourceData) {
// Split the data into main and alt displays
data = haveSplit ? splitFn(sourceData, key => !splitter.isOnAltAxis(key)) : sourceData;
altData = haveSplit ? splitFn(sourceData, splitter.isOnAltAxis) : null;
}

splitter.color = (...args) => {
if (!args.length) {
Expand All @@ -55,8 +64,6 @@ export const axisSplitter = (settings, sourceData, splitFn = dataSplitFunction)
return splitter;
};

splitter.haveSplit = () => haveSplit;

splitter.data = (...args) => {
if (!args.length) {
return data;
Expand All @@ -71,6 +78,13 @@ export const axisSplitter = (settings, sourceData, splitFn = dataSplitFunction)
altData = args[0];
return splitter;
};
splitter.decorate = (...args) => {
if (!args.length) {
return decorate;
}
decorate = args[0];
return splitter;
};

return splitter;
};
Expand All @@ -79,7 +93,7 @@ export const dataSplitFunction = (sourceData, isIncludedFn) => {
return sourceData.map(d => d.filter(v => isIncludedFn(v.key)));
};

export const dataBlankFunction = (sourceData, isIncludedFn) => {
export const groupBlankFunction = (sourceData, isIncludedFn) => {
return sourceData.map(series => {
if (!isIncludedFn(series.key)) {
// Blank this data
Expand All @@ -89,6 +103,14 @@ export const dataBlankFunction = (sourceData, isIncludedFn) => {
});
};

export const groupedBlankFunction = (sourceData, isIncludedFn) => {
return sourceData.map(group => dataBlankFunction(group, isIncludedFn));
export const multiGroupBlankFunction = (sourceData, isIncludedFn) => {
return sourceData.map(group => groupBlankFunction(group, isIncludedFn));
};

export const groupRemoveFunction = (sourceData, isIncludedFn) => {
return sourceData.filter(series => isIncludedFn(series.key));
};

export const multiGroupRemoveFunction = (sourceData, isIncludedFn) => {
return sourceData.map(group => groupRemoveFunction(group, isIncludedFn));
};
38 changes: 30 additions & 8 deletions packages/perspective-viewer-d3fc/src/js/axis/chartFactory.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ export const chartCanvasFactory = (xAxis, yAxis) => chartFactory(xAxis, yAxis, f
const chartFactory = (xAxis, yAxis, cartesian, canvas) => {
let axisSplitter = null;
let altAxis = null;
let altPlotArea = null;
let altXScale = null;

const chart = cartesian({
xScale: xAxis.scale,
Expand Down Expand Up @@ -59,6 +61,22 @@ const chartFactory = (xAxis, yAxis, cartesian, canvas) => {
return chart;
};

chart.altPlotArea = (...args) => {
if (!args.length) {
return altPlotArea;
}
altPlotArea = args[0];
return chart;
};

chart.altXScale = (...args) => {
if (!args.length) {
return altXScale;
}
altXScale = args[0];
return chart;
};

const oldDecorate = chart.decorate();
chart.decorate((container, data) => {
oldDecorate(container, data);
Expand Down Expand Up @@ -112,14 +130,19 @@ const chartFactory = (xAxis, yAxis, cartesian, canvas) => {
});

// Render all the series using either the primary or alternate y-scales
const plotAreas = [chart.plotArea(), altPlotArea || chart.plotArea()];
const xScales = [xAxis.scale, altXScale || xAxis.scale];
const yScales = [yAxis.scale, y2Scale];
if (canvas) {
const drawMultiCanvasSeries = selection => {
const canvasPlotArea = chart.plotArea();
canvasPlotArea.context(selection.node().getContext("2d")).xScale(xAxis.scale);
if (altXScale) altXScale.range(xAxis.scale.range());

const yScales = [yAxis.scale, y2Scale];
[data, altData].forEach((d, i) => {
canvasPlotArea.yScale(yScales[i]);
const canvasPlotArea = plotAreas[i];
canvasPlotArea
.context(selection.node().getContext("2d"))
.xScale(xScales[i])
.yScale(yScales[i]);
canvasPlotArea(d);
});
};
Expand All @@ -129,12 +152,11 @@ const chartFactory = (xAxis, yAxis, cartesian, canvas) => {
});
} else {
const drawMultiSvgSeries = selection => {
const svgPlotArea = chart.plotArea();
svgPlotArea.xScale(xAxis.scale);
if (altXScale) altXScale.range(xAxis.scale.range());

const yScales = [yAxis.scale, y2Scale];
ySeriesDataJoin(selection, [data, altData]).each((d, i, nodes) => {
svgPlotArea.yScale(yScales[i]);
const svgPlotArea = plotAreas[i];
svgPlotArea.xScale(xScales[i]).yScale(yScales[i]);
d3.select(nodes[i])
.datum(d)
.call(svgPlotArea);
Expand Down
7 changes: 0 additions & 7 deletions packages/perspective-viewer-d3fc/src/js/axis/ordinalAxis.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,6 @@ export const component = settings => {
};
};

const pickAxis = multiLevel => {
if (multiLevel) {
return orient === "horizontal" ? multiAxisBottom : multiAxisLeft;
}
return orient === "horizontal" ? fc.axisOrdinalBottom : fc.axisOrdinalLeft;
};

const getAxisSet = multiLevel => {
if (multiLevel) {
return {
Expand Down
21 changes: 15 additions & 6 deletions packages/perspective-viewer-d3fc/src/js/charts/area.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import * as fc from "d3fc";
import {axisFactory} from "../axis/axisFactory";
import {chartSvgFactory} from "../axis/chartFactory";
import {axisSplitter} from "../axis/axisSplitter";
import {axisSplitter, dataSplitFunction} from "../axis/axisSplitter";
import {AXIS_TYPES} from "../axis/axisType";
import {areaSeries} from "../series/areaSeries";
import {seriesColors} from "../series/seriesColors";
Expand All @@ -23,15 +23,13 @@ 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 {data, series, splitFn} = getDataAndSeries(settings, color);

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")
Expand All @@ -45,7 +43,7 @@ function areaChart(container, settings) {
.paddingStrategy(hardLimitZeroPadding());

// Check whether we've split some values into a second y-axis
const splitter = axisSplitter(settings, data).color(color);
const splitter = axisSplitter(settings, data, splitFn).color(color);

const yAxis1 = yAxisFactory(splitter.data());

Expand Down Expand Up @@ -90,3 +88,14 @@ areaChart.plugin = {
};

export default areaChart;

const getData = settings => splitAndBaseData(settings, filterData(settings));
const getSeries = (settings, color) => fc.seriesSvgRepeat().series(areaSeries(settings, color).orient("vertical"));

export const getDataAndSeries = (settings, color) => {
return {
data: getData(settings),
series: getSeries(settings, color),
splitFn: dataSplitFunction
};
};
3 changes: 2 additions & 1 deletion packages/perspective-viewer-d3fc/src/js/charts/charts.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*
*/

import multiChart from "./multi";
import barChart from "./bar";
import columnChart from "./column";
import lineChart from "./line";
Expand All @@ -19,6 +20,6 @@ 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];
const chartClasses = [multiChart, barChart, columnChart, lineChart, areaChart, yScatter, xyScatter, heatmap, ohlc, candlestick, sunburst, treemap];

export default chartClasses;
60 changes: 44 additions & 16 deletions packages/perspective-viewer-d3fc/src/js/charts/column.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ 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 {axisSplitter, groupBlankFunction, multiGroupBlankFunction, groupRemoveFunction, multiGroupRemoveFunction} from "../axis/axisSplitter";
import {axisType, AXIS_TYPES} from "../axis/axisType";
import {barSeries} from "../series/barSeries";
import {seriesColors} from "../series/seriesColors";
import {groupAndStackData} from "../data/groupData";
Expand All @@ -22,23 +22,18 @@ 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 {data, series, splitFn, xScaleFn} = getDataAndSeries(settings, color);

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")
Expand All @@ -48,8 +43,7 @@ function columnChart(container, settings) {
.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 splitter = axisSplitter(settings, data, splitFn).color(color);

const yAxis1 = yAxisFactory(splitter.data());

Expand All @@ -60,11 +54,8 @@ function columnChart(container, settings) {
.axisSplitter(splitter)
.plotArea(plotSeries);

if (chart.xPaddingInner) {
chart.xPaddingInner(0.5);
chart.xPaddingOuter(0.25);
bars.align("left");
}
xScaleFn(xAxis.scale);

chart.yNice && chart.yNice();

const zoomChart = zoomableChart()
Expand All @@ -91,3 +82,40 @@ columnChart.plugin = {
};

export default columnChart;

const getData = settings => groupAndStackData(settings, filterData(settings));
const getSeries = (settings, data, color, options = {mixCharts: false}) => {
const bars = barSeries(settings, color, options).orient("vertical");

if (axisType(settings).excludeType(AXIS_TYPES.linear)() == AXIS_TYPES.ordinal) {
bars.align("left");
}

return fc
.seriesSvgMulti()
.mapping((data, index) => data[index])
.series(data.map(() => bars));
};

const getSplitFn = (settings, options) => {
const grouped = settings.mainValues.length > 1;
if (options.mixCharts) {
return grouped ? multiGroupRemoveFunction : groupRemoveFunction;
}
return grouped ? multiGroupBlankFunction : groupBlankFunction;
};

export const getDataAndSeries = (settings, color, symbols, options = {mixCharts: false}) => {
const data = getData(settings);
return {
data,
series: getSeries(settings, data, color, options),
splitFn: getSplitFn(settings, options),
xScaleFn: scale => {
if (scale.paddingInner) {
scale.paddingInner(0.5);
scale.paddingOuter(0.25);
}
}
};
};