Jitter layout (#476)
* Jitter layout * Fix default chart * Add const for radius
This commit is contained in:
Родитель
3209f04234
Коммит
8268a413fc
|
@ -0,0 +1,10 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
|
||||
<title>icons</title>
|
||||
<circle cx="10" cy="4" r="2.33333" style="fill-opacity:0.0;stroke:#000;stroke-width:1px" />
|
||||
<circle cx="20" cy="11" r="2.33333" style="fill-opacity:0.0;stroke:#000;stroke-width:1px" />
|
||||
<circle cx="8" cy="22" r="2.33333" style="fill-opacity:0.0;stroke:#000;stroke-width:1px" />
|
||||
<circle cx="23" cy="22" r="2.33333" style="fill-opacity:0.0;stroke:#000;stroke-width:1px" />
|
||||
<circle cx="4" cy="12" r="2.33333" style="fill-opacity:0.0;stroke:#000;stroke-width:1px" />
|
||||
<circle cx="14" cy="16" r="2.33333" style="fill-opacity:0.0;stroke:#000;stroke-width:1px" />
|
||||
<circle cx="21" cy="3" r="2.33333" style="fill-opacity:0.0;stroke:#000;stroke-width:1px" />
|
||||
</svg>
|
После Ширина: | Высота: | Размер: 767 B |
|
@ -288,6 +288,10 @@ addSVGIcon(
|
|||
"sublayout/packing",
|
||||
require("resources/icons/icons_sublayout-packing.svg")
|
||||
);
|
||||
addSVGIcon(
|
||||
"sublayout/jitter",
|
||||
require("resources/icons/icons_sublayout-jitter.svg")
|
||||
);
|
||||
addSVGIcon("align/left", require("resources/icons/icons_align-left.svg"));
|
||||
addSVGIcon(
|
||||
"align/x-middle",
|
||||
|
|
|
@ -87,6 +87,10 @@ export function createDefaultPlotSegment(
|
|||
gravityX: 0.1,
|
||||
gravityY: 0.1,
|
||||
},
|
||||
jitter: {
|
||||
vertical: true,
|
||||
horizontal: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
} as Specification.PlotSegment;
|
||||
|
|
|
@ -464,7 +464,7 @@ export abstract class HashMap<KeyType, ValueType> {
|
|||
export class MultistringHashMap<ValueType> extends HashMap<
|
||||
string[],
|
||||
ValueType
|
||||
> {
|
||||
> {
|
||||
protected separator: string = Math.random().toString(36).substr(2);
|
||||
protected hash(key: string[]): string {
|
||||
return key.join(this.separator);
|
||||
|
@ -652,18 +652,18 @@ let formatOptions: FormatLocaleDefinition = {
|
|||
decimal: ".",
|
||||
thousands: ",",
|
||||
grouping: [3],
|
||||
currency: ["$", ""]
|
||||
currency: ["$", ""],
|
||||
};
|
||||
|
||||
export function getFormatOptions(): FormatLocaleDefinition {
|
||||
return {
|
||||
...formatOptions
|
||||
...formatOptions,
|
||||
};
|
||||
}
|
||||
|
||||
export function setFormatOptions(options: FormatLocaleDefinition) {
|
||||
formatOptions = {
|
||||
...options
|
||||
...options,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -674,7 +674,11 @@ export function getFormat() {
|
|||
export function parseSafe(value: string, defaultValue: any = null) {
|
||||
try {
|
||||
return JSON.parse(value);
|
||||
} catch(ex) {
|
||||
} catch (ex) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
export function getRandom(startRange: number, endRange: number) {
|
||||
return startRange + Math.random() * (endRange - startRange);
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ import { PlotSegmentClass } from "../plot_segment";
|
|||
import { strings } from "./../../../../strings";
|
||||
|
||||
export interface Region2DSublayoutOptions extends Specification.AttributeMap {
|
||||
type: "overlap" | "dodge-x" | "dodge-y" | "grid" | "packing";
|
||||
type: "overlap" | "dodge-x" | "dodge-y" | "grid" | "packing" | "jitter";
|
||||
|
||||
/** Sublayout alignment (for dodge and grid) */
|
||||
align: {
|
||||
|
@ -50,6 +50,10 @@ export interface Region2DSublayoutOptions extends Specification.AttributeMap {
|
|||
gravityX: number;
|
||||
gravityY: number;
|
||||
};
|
||||
jitter: {
|
||||
vertical: boolean;
|
||||
horizontal: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Region2DAttributes extends Specification.AttributeMap {
|
||||
|
@ -106,6 +110,7 @@ export interface Region2DConfigurationTerminology {
|
|||
/** Packing force layout */
|
||||
packing: string;
|
||||
overlap: string;
|
||||
jitter: string;
|
||||
}
|
||||
|
||||
export interface Region2DConfigurationIcons {
|
||||
|
@ -119,6 +124,7 @@ export interface Region2DConfigurationIcons {
|
|||
dodgeYIcon: string;
|
||||
gridIcon: string;
|
||||
packingIcon: string;
|
||||
jitterIcon: string;
|
||||
overlapIcon: string;
|
||||
}
|
||||
|
||||
|
@ -232,6 +238,8 @@ export class Region2DConstraintBuilder {
|
|||
public solverContext?: BuildConstraintsContext
|
||||
) {}
|
||||
|
||||
public static defaultJitterPackingRadius = 5;
|
||||
|
||||
public getTableContext(): DataflowTable {
|
||||
return this.plotSegment.parent.dataflow.getTable(
|
||||
this.plotSegment.object.table
|
||||
|
@ -1093,6 +1101,8 @@ export class Region2DConstraintBuilder {
|
|||
if (context.mode == "x-only" || context.mode == "y-only") {
|
||||
if (props.sublayout.type == "packing") {
|
||||
this.sublayoutPacking(groups, context.mode == "x-only" ? "x" : "y");
|
||||
} else if (props.sublayout.type == "jitter") {
|
||||
this.sublayoutJitter(groups, context.mode == "x-only" ? "x" : "y");
|
||||
} else {
|
||||
this.fitGroups(groups, axis);
|
||||
}
|
||||
|
@ -1116,6 +1126,10 @@ export class Region2DConstraintBuilder {
|
|||
if (props.sublayout.type == "packing") {
|
||||
this.sublayoutPacking(groups);
|
||||
}
|
||||
// Jitter layout
|
||||
if (props.sublayout.type == "jitter") {
|
||||
this.sublayoutJitter(groups);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1739,7 +1753,7 @@ export class Region2DConstraintBuilder {
|
|||
}
|
||||
}
|
||||
if (radius == 0) {
|
||||
radius = 5;
|
||||
radius = Region2DConstraintBuilder.defaultJitterPackingRadius;
|
||||
}
|
||||
return [
|
||||
solver.attr(state.attributes, "x"),
|
||||
|
@ -1764,6 +1778,60 @@ export class Region2DConstraintBuilder {
|
|||
});
|
||||
}
|
||||
|
||||
public sublayoutJitter(groups: SublayoutGroup[], axisOnly?: "x" | "y") {
|
||||
const solver = this.solver;
|
||||
const state = this.plotSegment.state;
|
||||
const jitterProps = this.plotSegment.object.properties.sublayout.jitter;
|
||||
|
||||
groups.forEach((group) => {
|
||||
const markStates = group.group.map((index) => state.glyphs[index]);
|
||||
const { x1, y1, x2, y2 } = group;
|
||||
|
||||
const points = markStates.map((state) => {
|
||||
let radius = 0;
|
||||
for (const e of state.marks) {
|
||||
if (e.attributes.size != null) {
|
||||
radius = Math.max(
|
||||
radius,
|
||||
Math.sqrt((e.attributes.size as number) / Math.PI)
|
||||
);
|
||||
} else {
|
||||
const w = e.attributes.width as number;
|
||||
const h = e.attributes.height as number;
|
||||
if (w != null && h != null) {
|
||||
radius = Math.max(radius, Math.sqrt(w * w + h * h) / 2);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (radius == 0) {
|
||||
radius = Region2DConstraintBuilder.defaultJitterPackingRadius;
|
||||
}
|
||||
return [
|
||||
solver.attr(state.attributes, "x"),
|
||||
solver.attr(state.attributes, "y"),
|
||||
radius,
|
||||
] as [Variable, Variable, number];
|
||||
});
|
||||
solver.addPlugin(
|
||||
new ConstraintPlugins.JitterPlugin(
|
||||
solver,
|
||||
x1,
|
||||
y1,
|
||||
x2,
|
||||
y2,
|
||||
points,
|
||||
axisOnly,
|
||||
jitterProps
|
||||
? jitterProps
|
||||
: {
|
||||
horizontal: true,
|
||||
vertical: true,
|
||||
}
|
||||
)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
public getHandles(): Region2DHandleDescription[] {
|
||||
const state = this.plotSegment.state;
|
||||
const props = this.plotSegment.object.properties;
|
||||
|
@ -2094,6 +2162,11 @@ export class Region2DConstraintBuilder {
|
|||
label: terminology.grid,
|
||||
icon: icons.gridIcon,
|
||||
};
|
||||
const jitterOption = {
|
||||
value: "jitter",
|
||||
label: terminology.jitter,
|
||||
icon: icons.jitterIcon,
|
||||
};
|
||||
const props = this.plotSegment.object.properties;
|
||||
const xMode = props.xData ? props.xData.type : "null";
|
||||
const yMode = props.yData ? props.yData.type : "null";
|
||||
|
@ -2107,9 +2180,10 @@ export class Region2DConstraintBuilder {
|
|||
dodgeYOption,
|
||||
gridOption,
|
||||
packingOption,
|
||||
jitterOption,
|
||||
];
|
||||
}
|
||||
return [overlapOption, packingOption];
|
||||
return [overlapOption, packingOption, jitterOption];
|
||||
}
|
||||
|
||||
public isSublayoutApplicable() {
|
||||
|
@ -2275,6 +2349,24 @@ export class Region2DConstraintBuilder {
|
|||
)
|
||||
);
|
||||
}
|
||||
if (type == "jitter") {
|
||||
extra.push(
|
||||
m.row(
|
||||
"Distribution",
|
||||
m.horizontal(
|
||||
[0, 1, 1],
|
||||
m.inputBoolean(
|
||||
{ property: "sublayout", field: ["jitter", "horizontal"] },
|
||||
{ type: "highlight", icon: "sublayout/dodge-x" }
|
||||
),
|
||||
m.inputBoolean(
|
||||
{ property: "sublayout", field: ["jitter", "vertical"] },
|
||||
{ type: "highlight", icon: "sublayout/dodge-y" }
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
const options = this.applicableSublayoutOptions();
|
||||
return [
|
||||
m.sectionHeader("Sub-layout"),
|
||||
|
|
|
@ -58,6 +58,7 @@ const icons: Region2DConfigurationIcons = {
|
|||
dodgeYIcon: "sublayout/dodge-y",
|
||||
gridIcon: "sublayout/grid",
|
||||
packingIcon: "sublayout/packing",
|
||||
jitterIcon: "sublayout/jitter",
|
||||
overlapIcon: "sublayout/overlap",
|
||||
};
|
||||
|
||||
|
@ -86,7 +87,7 @@ export class CartesianPlotSegment extends PlotSegmentClass<
|
|||
|
||||
public static defaultMappingValues: Specification.AttributeMap = {};
|
||||
|
||||
public static defaultProperties: Specification.AttributeMap = {
|
||||
public static defaultProperties: CartesianProperties = {
|
||||
marginX1: 0,
|
||||
marginY1: 0,
|
||||
marginX2: 0,
|
||||
|
@ -106,6 +107,15 @@ export class CartesianPlotSegment extends PlotSegmentClass<
|
|||
xCount: null,
|
||||
yCount: null,
|
||||
},
|
||||
jitter: {
|
||||
horizontal: true,
|
||||
vertical: true,
|
||||
},
|
||||
packing: {
|
||||
gravityX: 0.1,
|
||||
gravityY: 0.1,
|
||||
},
|
||||
orderReversed: null,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -73,6 +73,7 @@ export let icons: Region2DConfigurationIcons = {
|
|||
dodgeYIcon: "sublayout/dodge-y",
|
||||
gridIcon: "sublayout/grid",
|
||||
packingIcon: "sublayout/packing",
|
||||
jitterIcon: "sublayout/jitter",
|
||||
overlapIcon: "sublayout/overlap",
|
||||
};
|
||||
|
||||
|
|
|
@ -86,6 +86,7 @@ export let icons: Region2DConfigurationIcons = {
|
|||
gridIcon: "sublayout/polar-grid",
|
||||
packingIcon: "sublayout/packing",
|
||||
overlapIcon: "sublayout/overlap",
|
||||
jitterIcon: "sublayout/jitter",
|
||||
};
|
||||
|
||||
export class PolarPlotSegment extends PlotSegmentClass<
|
||||
|
|
|
@ -3,3 +3,4 @@
|
|||
export { PackingPlugin } from "./packing";
|
||||
export { PolarCoordinatorPlugin } from "./polar_coordinator";
|
||||
export { PolarPlotSegmentPlugin } from "./polar_plotsegment";
|
||||
export { JitterPlugin } from "./jitter";
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
// Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
// Licensed under the MIT license.
|
||||
import { forceSimulation, forceCollide, forceX, forceY } from "d3-force";
|
||||
import { getRandom } from "../..";
|
||||
import { ConstraintPlugin, ConstraintSolver, Variable } from "../abstract";
|
||||
|
||||
interface NodeType {
|
||||
x?: number;
|
||||
y?: number;
|
||||
}
|
||||
|
||||
export interface JitterPluginOptions {
|
||||
vertical: boolean;
|
||||
horizontal: boolean;
|
||||
}
|
||||
|
||||
export class JitterPlugin extends ConstraintPlugin {
|
||||
public solver: ConstraintSolver;
|
||||
public x1: Variable;
|
||||
public y1: Variable;
|
||||
public x2: Variable;
|
||||
public y2: Variable;
|
||||
public points: Array<[Variable, Variable, number]>;
|
||||
public xEnable: boolean;
|
||||
public yEnable: boolean;
|
||||
public getXYScale: () => { x: number; y: number };
|
||||
public options?: JitterPluginOptions;
|
||||
|
||||
constructor(
|
||||
solver: ConstraintSolver,
|
||||
x1: Variable,
|
||||
y1: Variable,
|
||||
x2: Variable,
|
||||
y2: Variable,
|
||||
points: Array<[Variable, Variable, number]>,
|
||||
axisOnly?: "x" | "y",
|
||||
options?: JitterPluginOptions
|
||||
) {
|
||||
super();
|
||||
this.solver = solver;
|
||||
this.x1 = x1;
|
||||
this.y1 = y1;
|
||||
this.x2 = x2;
|
||||
this.y2 = y2;
|
||||
this.points = points;
|
||||
this.xEnable = axisOnly == null || axisOnly == "x";
|
||||
this.yEnable = axisOnly == null || axisOnly == "y";
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
public apply() {
|
||||
const x1 = this.solver.getValue(this.x1);
|
||||
const x2 = this.solver.getValue(this.x2);
|
||||
const y1 = this.solver.getValue(this.y1);
|
||||
const y2 = this.solver.getValue(this.y2);
|
||||
const nodes = this.points.map((pt) => {
|
||||
const x = getRandom(x1, x2);
|
||||
const y = getRandom(y1, y2);
|
||||
// Use forceSimulation's default initialization
|
||||
return {
|
||||
x,
|
||||
y,
|
||||
} as NodeType;
|
||||
});
|
||||
|
||||
for (let i = 0; i < nodes.length; i++) {
|
||||
if (this.options.horizontal) {
|
||||
this.solver.setValue(this.points[i][0], nodes[i].x);
|
||||
}
|
||||
if (this.options.vertical) {
|
||||
this.solver.setValue(this.points[i][1], nodes[i].y);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -20,6 +20,7 @@ const cartesianTerminology: Region2DConfigurationTerminology = {
|
|||
gridDirectionX: "X",
|
||||
gridDirectionY: "Y",
|
||||
packing: "Packing",
|
||||
jitter: "Jitter",
|
||||
overlap: "Overlap",
|
||||
};
|
||||
|
||||
|
@ -38,6 +39,7 @@ const curveTerminology: Region2DConfigurationTerminology = {
|
|||
gridDirectionX: "Tangent",
|
||||
gridDirectionY: "Normal",
|
||||
packing: "Packing",
|
||||
jitter: "Jitter",
|
||||
overlap: "Overlap",
|
||||
};
|
||||
|
||||
|
@ -56,6 +58,7 @@ const polarTerminology: Region2DConfigurationTerminology = {
|
|||
gridDirectionX: "Angular",
|
||||
gridDirectionY: "Radial",
|
||||
packing: "Packing",
|
||||
jitter: "Jitter",
|
||||
overlap: "Overlap",
|
||||
};
|
||||
|
||||
|
|
Загрузка…
Ссылка в новой задаче