Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/perspective-viewer-d3fc/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@
"chroma-js": "^1.3.4",
"d3": "^5.7.0",
"d3-svg-legend": "^2.25.6",
"d3fc": "^14.0.27",
"d3fc": "^14.0.30",
"gradient-parser": "0.1.5"
},
"devDependencies": {
Expand Down
4 changes: 1 addition & 3 deletions packages/perspective-viewer-d3fc/src/html/tooltip.html
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
<ul id="cross-values"></ul>
<ul id="split-values"></ul>
<ul id="data-values"></ul>
<ul id="tooltip-values"></ul>
234 changes: 145 additions & 89 deletions packages/perspective-viewer-d3fc/src/js/axis/crossAxis.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import * as d3 from "d3";
import * as fc from "d3fc";
import minBandwidth from "./minBandwidth";
import withoutTicks from "./withoutTicks";
import {axisOrdinalBottom, axisOrdinalLeft} from "../d3fc/axis/axisOrdinal";
import {multiAxisBottom, multiAxisLeft} from "../d3fc/axis/multi-axis";

const AXIS_TYPES = {
Expand All @@ -34,14 +35,22 @@ export const scale = (settings, settingName = "crossValues") => {

const defaultScaleBand = () => minBandwidth(d3.scaleBand());

const flattenArray = array => {
if (Array.isArray(array)) {
return [].concat(...array.map(flattenArray));
} else {
return [array];
}
};

export const domain = settings => {
let valueName = "crossValue";
let settingName = "crossValues";

const extentTime = fc.extentTime().accessors([d => new Date(d[valueName])]);

const _domain = function(data) {
const flattenedData = data.flat(2);
const flattenedData = flattenArray(data);
switch (axisType(settings, settingName)) {
case AXIS_TYPES.time:
return extentTime(flattenedData);
Expand Down Expand Up @@ -101,12 +110,87 @@ const axisType = (settings, settingName = "crossValues") => {
return AXIS_TYPES.ordinal;
};

export const styleAxis = (chart, prefix, settings, settingName = "crossValues") => {
export const axisFactory = settings => {
let orient = "horizontal";
let settingName = "crossValues";
let domain = null;

const factory = () => {
switch (axisType(settings, settingName)) {
case AXIS_TYPES.ordinal:
const multiLevel = settings[settingName].length > 1 && settings[settingName].every(v => v.type !== "datetime");

// 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 = scale => {
const axis = pickAxis(multiLevel)(scale);
axis.tickLineAlign("right").tickPadding(8);

if (multiLevel) {
axis.groups(levelGroups)
.tickSizeInner(tickSizeInner)
.tickSizeOuter(tickSizeOuter);
}
return axis;
};

const decorate = (s, data, index) => {
const rotated = groupTickLayout[index].rotate;
hideOverlappingLabels(s, rotated);
if (orient === "horizontal") applyLabelRotation(s, rotated);
};

return {
bottom: createAxis,
left: createAxis,
size: `${tickSizeOuter + 10}px`,
decorate
};
}

// Default axis
return {
bottom: fc.axisBottom,
left: fc.axisLeft,
decorate: () => {}
};
};

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

const axisGroups = domain => {
const groups = [];
domain.forEach(tick => {
const split = tick.split("|");
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.length));

if (prefix === "x") {
if (orient === "horizontal") {
// x-axis may rotate labels and expand the available height
if (group.length * (maxLength * 6 + 10) > width - 100) {
return {
Expand All @@ -127,101 +211,73 @@ export const styleAxis = (chart, prefix, settings, settingName = "crossValues")
}
};

chart[`${prefix}Label`](label(settings, settingName));
const hideOverlappingLabels = (s, rotated) => {
const getTransformCoords = transform =>
transform
.substring(transform.indexOf("(") + 1, transform.indexOf(")"))
.split(",")
.map(c => parseInt(c));

const suppliedDomain = chart[`${prefix}Domain`]();
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 + 14 > r2.x;

switch (axisType(settings, settingName)) {
case AXIS_TYPES.ordinal:
const multiLevel = settings[settingName].length > 1 && settings[settingName].every(v => v.type !== "datetime");

// Calculate the label groups and corresponding group sizes
const levelGroups = axisGroups(suppliedDomain);
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);

chart[`${prefix}CenterAlignTicks`](true)
[`${prefix}TickSizeInner`](tickSizeInner)
[`${prefix}TickSizeOuter`](tickSizeOuter)
[`${prefix}TickPadding`](8)
[`${prefix}Axis${prefix === "x" ? "Height" : "Width"}`](tickSizeOuter + 10)
[`${prefix}Decorate`]((s, data, index) => {
const rotated = groupTickLayout[index].rotate;
hideOverlappingLabels(s, rotated);
if (prefix === "x") applyLabelRotation(s, rotated);
});

if (multiLevel) {
chart[`${prefix}Axis`](scale => {
const multiAxis = prefix === "x" ? multiAxisBottom(scale) : multiAxisLeft(scale);
multiAxis.groups(levelGroups);
return multiAxis;
});
const previousRectangles = [];
s.each((d, i, nodes) => {
const tick = d3.select(nodes[i]);
const text = tick.select("text");

const transformCoords = getTransformCoords(tick.attr("transform"));

let rect = {};
let overlap = false;
if (rotated) {
rect = {x: transformCoords[0], y: transformCoords[1]};
overlap = previousRectangles.some(r => rotatedLabelsOverlap(r, rect));
} else {
const textRect = text.node().getBBox();
rect = {x: textRect.x + transformCoords[0], y: textRect.y + transformCoords[1], width: textRect.width, height: textRect.height};
overlap = previousRectangles.some(r => rectanglesOverlap(r, rect));
}
break;
}
};

function hideOverlappingLabels(s, rotated) {
const getTransformCoords = transform =>
transform
.substring(transform.indexOf("(") + 1, transform.indexOf(")"))
.split(",")
.map(c => parseInt(c));
text.attr("visibility", overlap ? "hidden" : "");
if (!overlap) {
previousRectangles.push(rect);
}
});
};

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 + 14 > r2.x;
const applyLabelRotation = (s, rotate) => {
s.each((d, i, nodes) => {
const tick = d3.select(nodes[i]);
const text = tick.select("text");

const previousRectangles = [];
s.each((d, i, nodes) => {
const tick = d3.select(nodes[i]);
const text = tick.select("text");
text.attr("transform", rotate ? "rotate(-45 5 5)" : "translate(0, 8)").style("text-anchor", rotate ? "end" : "");
});
};

const transformCoords = getTransformCoords(tick.attr("transform"));
factory.orient = (...args) => {
if (!args.length) {
return orient;
}
orient = args[0];
return factory;
};

let rect = {};
let overlap = false;
if (rotated) {
rect = {x: transformCoords[0], y: transformCoords[1]};
overlap = previousRectangles.some(r => rotatedLabelsOverlap(r, rect));
} else {
const textRect = text.node().getBBox();
rect = {x: textRect.x + transformCoords[0], y: textRect.y + transformCoords[1], width: textRect.width, height: textRect.height};
overlap = previousRectangles.some(r => rectanglesOverlap(r, rect));
factory.settingName = (...args) => {
if (!args.length) {
return settingName;
}
settingName = args[0];
return factory;
};

text.attr("visibility", overlap ? "hidden" : "");
if (!overlap) {
previousRectangles.push(rect);
factory.domain = (...args) => {
if (!args.length) {
return domain;
}
});
}

function applyLabelRotation(s, rotate) {
s.each((d, i, nodes) => {
const tick = d3.select(nodes[i]);
const text = tick.select("text");

text.attr("transform", rotate ? "rotate(-45 5 5)" : "translate(0, 8)").style("text-anchor", rotate ? "end" : "");
});
}

const axisGroups = domain => {
const groups = [];
domain.forEach(tick => {
const split = tick.split("|");
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();
domain = args[0];
return factory;
};

return factory;
};
4 changes: 0 additions & 4 deletions packages/perspective-viewer-d3fc/src/js/axis/mainAxis.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,3 @@ function flattenExtent(array) {
};
return array.reduce((r, v) => [withUndefined(Math.min)(r[0], v[0]), withUndefined(Math.max)(r[1], v[1])], [undefined, undefined]);
}

export const styleAxis = (chart, prefix, settings) => {
chart[`${prefix}Label`](label(settings));
};
43 changes: 31 additions & 12 deletions packages/perspective-viewer-d3fc/src/js/charts/area.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,42 +10,53 @@ import * as fc from "d3fc";
import * as crossAxis from "../axis/crossAxis";
import * as mainAxis from "../axis/mainAxis";
import {areaSeries} from "../series/areaSeries";
import {seriesColours} from "../series/seriesColours";
import {seriesColors} from "../series/seriesColors";
import {splitAndBaseData} from "../data/splitAndBaseData";
import {colourLegend} from "../legend/legend";
import {colorLegend} from "../legend/legend";
import {filterData} from "../legend/filter";
import {withGridLines} from "../gridlines/gridlines";

import chartSvgCartesian from "../d3fc/chart/svg/cartesian";
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 colour = seriesColours(settings);
const legend = colourLegend()
const color = seriesColors(settings);
const legend = colorLegend()
.settings(settings)
.scale(colour);
.scale(color);

const series = fc.seriesSvgRepeat().series(areaSeries(settings, colour).orient("vertical"));
const series = fc.seriesSvgRepeat().series(areaSeries(settings, color).orient("vertical"));

const xDomain = crossAxis.domain(settings)(data);
const xScale = crossAxis.scale(settings);
const chart = chartSvgCartesian(xScale, mainAxis.scale(settings))
.xDomain(crossAxis.domain(settings)(data))
const yScale = mainAxis.scale(settings);
const xAxis = crossAxis.axisFactory(settings).domain(xDomain)();

const chart = fc
.chartSvgCartesian({
xScale,
yScale,
xAxis
})
.xDomain(xDomain)
.xLabel(crossAxis.label(settings))
.xAxisHeight(xAxis.size)
.xDecorate(xAxis.decorate)
.yDomain(
mainAxis
.domain(settings)
.include([0])
.paddingStrategy(hardLimitZeroPadding())(data)
)
.yLabel(crossAxis.label(settings))
.yOrient("left")
.yLabel(mainAxis.label(settings))
.yNice()
.plotArea(withGridLines(series).orient("vertical"));

crossAxis.styleAxis(chart, "x", settings);
mainAxis.styleAxis(chart, "y", settings);

chart.xPaddingInner && chart.xPaddingInner(1);
chart.xPaddingOuter && chart.xPaddingOuter(0.5);

Expand All @@ -54,8 +65,16 @@ function areaChart(container, settings) {
.settings(settings)
.xScale(xScale);

const toolTip = nearbyTip()
.settings(settings)
.xScale(xScale)
.yScale(yScale)
.color(color)
.data(data);

// render
container.datum(data).call(zoomChart);
container.call(toolTip);
container.call(legend);
}
areaChart.plugin = {
Expand Down
Loading