Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
196 changes: 74 additions & 122 deletions packages/perspective-viewer-d3fc/src/js/charts/xy-scatter.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

import * as fc from "d3fc";
import * as d3 from "d3";
import { select } from "d3";
import { axisFactory } from "../axis/axisFactory";
import { chartCanvasFactory } from "../axis/chartFactory";
import {
Expand All @@ -20,8 +20,7 @@ import {
} from "../series/pointSeriesCanvas";
import { pointData } from "../data/pointData";
import {
seriesColorsFromField,
seriesColorsFromGroups,
seriesColorsFromColumn,
seriesColorsFromDistinct,
colorScale,
} from "../series/seriesColors";
Expand All @@ -34,24 +33,8 @@ import { hardLimitZeroPadding } from "../d3fc/padding/hardLimitZero";
import zoomableChart from "../zoom/zoomableChart";
import nearbyTip from "../tooltip/nearbyTip";
import { symbolsObj } from "../series/seriesSymbols";

/**
* Define a clamped scaling factor based on the container size for bubble plots.
*
* @param {Array} p1 a point as a tuple of `Number`
* @param {Array} p2 a second point as a tuple of `Number`
* @returns a function `container -> integer` which calculates a scaling factor
* from the linear function (clamped) defgined by the input points
*/
function interpolate_scale([x1, y1], [x2, y2]) {
const m = (y2 - y1) / (x2 - x1);
const b = y2 - m * x2;
return function (container) {
const node = container.node();
const shortest_axis = Math.min(node.clientWidth, node.clientHeight);
return Math.min(y2, Math.max(y1, m * shortest_axis + b));
};
}
import { gridLayoutMultiChart } from "../layout/gridLayoutMultiChart";
import xyScatterSeries from "../series/xy-scatter/xyScatterSeries";

/**
* Overrides specific symbols based on plugin settings. This modifies in-place _and_ returns the value.
Expand Down Expand Up @@ -86,133 +69,102 @@ function overrideSymbols(settings, symbols) {
return String(val) === String(key);
}
});
if (i === -1) {
console.error(
`Could not find row with value ${key} when overriding symbols!`
);
}

range[i] = symbolType;
});
symbols.range(range);
return symbols;
}

/**
* @param {d3.Selection} container - d3.Selection of the outer div
* @param {any} settings - settings as defined in the Update method in plugin.js
*/
function xyScatter(container, settings) {
const colorBy = settings.realValues[2];
let hasColorBy = !!colorBy;
let isColoredByString =
settings.mainValues.find((x) => x.name === colorBy)?.type === "string";

let color = null;
let legend = null;

const symbolCol = settings.realValues[4];
// TODO: This is failing to filter correctly when colorLegend() is called as it returns data meant for filterData
const data = pointData(settings, filterDataByGroup(settings));
const symbols = overrideSymbols(
settings,
symbolTypeFromColumn(settings, symbolCol)
);

let color = null;
let legend = null;

const colorByField = 2;
const colorByValue = settings.realValues[colorByField];
let hasColorBy = colorByValue !== null && colorByValue !== undefined;
let isColoredByString =
settings.mainValues.find((x) => x.name === colorByValue)?.type ===
"string";
let hasSymbol = !!symbolCol;
const data = pointData(settings, filterDataByGroup(settings));

if (hasColorBy) {
if (isColoredByString) {
if (hasSymbol) {
if (hasColorBy && isColoredByString) {
if (!!symbolCol) {
// TODO: Legend should have cartesian product labels (ColorBy|SplitBy)
// For now, just use monocolor legends.
if (settings.splitValues.length > 0) {
color = seriesColorsFromDistinct(settings, data);
// TODO: Legend should have cartesian product labels (ColorBy|Symbol)
// For now, just use monocolor legends.
legend = symbolLegend().settings(settings).scale(symbols);
} else {
color = seriesColorsFromField(settings, colorByField);
legend = colorLegend().settings(settings).scale(color);
color = seriesColorsFromDistinct(settings, data[0]);
}
legend = symbolLegend().settings(settings).scale(symbols);
} else {
color = seriesColorRange(settings, data, "colorValue");
legend = colorRangeLegend().scale(color);
color = seriesColorsFromColumn(settings, colorBy);
legend = colorLegend().settings(settings).scale(color);
}
} else if (hasColorBy) {
color = seriesColorRange(settings, data, "colorValue");
legend = colorRangeLegend().scale(color);
} else {
// always use default color
color = colorScale().settings(settings).domain([""])();
legend = symbolLegend().settings(settings).scale(symbols);
}

const size = settings.realValues[3]
? seriesLinearRange(settings, data, "size").range([10, 10000])
: null;

const label = settings.realValues[5];

const scale_factor = interpolate_scale([600, 0.1], [1600, 1])(container);
const series = fc
.seriesCanvasMulti()
.mapping((data, index) => data[index])
.series(
data.map((series) =>
pointSeriesCanvas(
settings,
symbolCol,
size,
color,
label,
symbols,
scale_factor
)
if (settings.splitValues.length !== 0) {
let xyGrid = gridLayoutMultiChart()
.svg(false)
.elementsPrefix("xy-scatter");
xyGrid = xyGrid.padding("2.5em");

const xLabel = container.append("div").attr("class", "multi-xlabel");
xLabel.append("p").text(settings.mainValues[0].name);

const yLabel = container.append("div").attr("class", "multi-ylabel");
yLabel
.append("p")
.text(settings.mainValues[1].name)
.style("transform", "rotate(-90deg)")
.style("text-wrap", "nowrap");

container = container.datum(data);
container.call(xyGrid);
const xyContainer = xyGrid.chartContainer();
const xyEnter = xyGrid.chartEnter();
const xyDiv = xyGrid.chartDiv();
const xyTitle = xyGrid.chartTitle();
const containerSize = xyGrid.containerSize();

xyTitle.each((d, i, nodes) => select(nodes[i]).text(d.key));
xyEnter
.merge(xyDiv)
.attr(
"transform",
`translate(${containerSize.width / 2}, ${
containerSize.height / 2
})`
)
);

const axisDefault = () =>
axisFactory(settings)
.settingName("mainValues")
.paddingStrategy(hardLimitZeroPadding())
.pad([0.1, 0.1]);

const xAxis = axisDefault()
.settingValue(settings.mainValues[0].name)
.memoValue(settings.axisMemo[0])
.valueName("x")(data);

const yAxis = axisDefault()
.orient("vertical")
.settingValue(settings.mainValues[1].name)
.memoValue(settings.axisMemo[1])
.valueName("y")(data);

const chart = chartCanvasFactory(xAxis, yAxis)
.xLabel(settings.mainValues[0].name)
.yLabel(settings.mainValues[1].name)
.plotArea(withGridLines(series, settings).canvas(true));

chart.xNice && chart.xNice();
chart.yNice && chart.yNice();

const zoomChart = zoomableChart()
.chart(chart)
.settings(settings)
.xScale(xAxis.scale)
.yScale(yAxis.scale)
.canvas(true);

const toolTip = nearbyTip()
.scaleFactor(scale_factor)
.settings(settings)
.canvas(true)
.xScale(xAxis.scale)
.xValueName("x")
.yValueName("y")
.yScale(yAxis.scale)
.color(!hasColorBy && color)
.size(size)
.data(data);

// render
container.datum(data).call(zoomChart);
container.call(toolTip);
.each(function (data) {
const xyElement = select(this);
xyScatterSeries()
.settings(settings)
.data([data])
.color(color)
.symbols(symbols)(xyElement);
});
} else {
xyScatterSeries()
.settings(settings)
.data(data)
.color(color)
.symbols(symbols)(container);
}

if (legend) container.call(legend);
}

Expand Down
27 changes: 27 additions & 0 deletions packages/perspective-viewer-d3fc/src/js/data/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +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](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

/**
* This function filters settings.data to get values corresponding to an input column.
* It accomodates for split-by.
* @param {*} settings
* @param {string} column
*/
export function getValuesByColumn(settings, column) {
return settings.data
.map((row) =>
Object.entries(row)
.filter(([k, v]) => k.split("|").at(-1) === column && !!v)
.map(([k, v]) => v)
)
.flat();
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,29 @@ export function gridLayoutMultiChart() {
let chartTitle = null;
let color = null;
let containerSize = null;
let svg = true;
let padding = null;

const _gridLayoutMultiChart = (container) => {
const innerContainer = getOrCreateElement(
const outerContainer = getOrCreateElement(
container,
"div.outer-container",
() =>
container
.append("div")
.attr("class", "outer-container")
.style("width", `calc(100% - ${padding ?? 0})`)
.style("height", `calc(100% - ${padding ?? 0})`)
.style("padding-left", padding ?? 0)
);

const scrollContainer = getOrCreateElement(
outerContainer,
"div.inner-container",
() => container.append("div").attr("class", "inner-container")
() => outerContainer.append("div").attr("class", "inner-container")
);

const innerRect = innerContainer.node().getBoundingClientRect();
const innerRect = outerContainer.node().getBoundingClientRect();
const containerHeight = innerRect.height;
const containerWidth = innerRect.width - (color ? 70 : 0);

Expand Down Expand Up @@ -58,20 +72,20 @@ export function gridLayoutMultiChart() {
}

if (data.length > 1) {
innerContainer.style(
scrollContainer.style(
"grid-template-columns",
`repeat(${cols}, ${100 / cols}%)`
);
innerContainer.style(
scrollContainer.style(
"grid-template-rows",
`repeat(${rows}, ${containerSize.height}px)`
);
} else {
innerContainer.style("grid-template-columns", `repeat(1, 100%)`);
innerContainer.style("grid-template-rows", `repeat(1, 100%)`);
scrollContainer.style("grid-template-columns", `repeat(1, 100%)`);
scrollContainer.style("grid-template-rows", `repeat(1, 100%)`);
}

chartDiv = innerContainer
chartDiv = scrollContainer
.selectAll(`div.${elementsPrefix}-container`)
.data(data, (d) => d.split);
chartDiv.exit().remove();
Expand All @@ -90,10 +104,30 @@ export function gridLayoutMultiChart() {
.attr("class", "title")
.style("text-align", "left");

chartContainer = chartEnter
.append("svg")
.append("g")
.attr("class", elementsPrefix);
if (svg) {
chartContainer = chartEnter
.append("svg")
.append("g")
.attr("class", elementsPrefix);
} else {
chartContainer = chartEnter
.append("div")
.attr("class", elementsPrefix);
}
};
_gridLayoutMultiChart.padding = (...args) => {
if (!args.length) {
return padding;
}
padding = args[0];
return _gridLayoutMultiChart;
};
_gridLayoutMultiChart.svg = (...args) => {
if (!args.length) {
return svg;
}
svg = args[0];
return _gridLayoutMultiChart;
};

_gridLayoutMultiChart.elementsPrefix = (...args) => {
Expand Down
Loading