Согласованный размер для GraphPlots

Обновление 10/27: в ответе я подробно описал шаги для достижения согласованной шкалы. По сути, для каждого объекта Graphics вам нужно зафиксировать все отступы/поля равными 0 и вручную указать plotRange и imageSize так, чтобы 1) plotRange включал всю графику 2) imageSize = scale * plotRange

Тем не менее, теперь вы знаете, как это сделать 1) в полном обобщении, решение, которое работает для графики, состоящей из точек и толстых линий (AbsoluteThickness), дано


Я использую "Inset" в VertexRenderingFunction и "VertexCoordinates", чтобы гарантировать согласованное появление среди подграфов графика. Эти подграфы нарисованы как вершины другого графа с использованием "Inset". Есть две проблемы, одна из которых заключается в том, что получившиеся блоки не обрезаются вокруг графа (т.е. граф с одной вершиной все еще помещается в большую рамку), а другая заключается в том, что существуют странные различия между размерами (вы можете видеть, что одна ячейка является вертикальной), Кто-нибудь может найти способ обойти эти проблемы?

Это связано с более ранним вопросом question о том, как сохранить размеры вершин одинаковыми, и хотя предложение Майкла Пилата об использовании Inset работает для сохранения вершин в одном и том же масштабе, общий масштаб может отличаться. Например, в левой ветки граф, состоящий из вершин 2,3, растянут относительно подграфа "2,3" в верхнем графе, хотя я использую абсолютное позиционирование вершин для обоих


(источник: yaroslavvb.com)

(*utilities*)intersect[a_, b_] := Select[a, MemberQ[b, #] &];
induced[s_] := Select[edges, #~intersect~s == # &];
Needs["GraphUtilities'"];
subgraphs[
   verts_] := (gr = 
    Rule @@@ Select[edges, (Intersection[#, verts] == #) &];
   Sort /@ WeakComponents[gr~Join~(# -> # & /@ verts)]);

(*graph*)
gname = {"Grid", {3, 3}};
edges = GraphData[gname, "EdgeIndices"];
nodes = Union[Flatten[edges]];
AppendTo[edges, #] & /@ ({#, #} & /@ nodes);
vcoords = Thread[nodes -> GraphData[gname, "VertexCoordinates"]];

(*decompose*)
edgesOuter = {};
pr[_, _, {}] := None;
pr[root_, elim_, 
   remain_] := (If[root != {}, AppendTo[edgesOuter, root -> remain]];
   pr[remain, intersect[Rest[elim], #], #] & /@ 
    subgraphs[Complement[remain, {First[elim]}]];);
pr[{}, {4, 5, 6, 1, 8, 2, 3, 7, 9}, nodes];

(*visualize*)

vrfInner = 
  Inset[Graphics[{White, EdgeForm[Black], Disk[{0, 0}, .05], Black, 
      Text[#2, {0, 0}]}, ImageSize -> 15], #] &;
vrfOuter = 
  Inset[GraphPlot[Rule @@@ induced[#2], 
     VertexRenderingFunction -> vrfInner, 
     VertexCoordinateRules -> vcoords, SelfLoopStyle -> None, 
     Frame -> True, ImageSize -> 100], #] &;
TreePlot[edgesOuter, Automatic, nodes, 
 EdgeRenderingFunction -> ({Red, Arrow[#1, 0.2]} &), 
 VertexRenderingFunction -> vrfOuter, ImageSize -> 500]

Вот еще один пример, та же проблема, что и раньше, но разница в относительных масштабах более заметна. Цель состоит в том, чтобы детали на втором изображении точно соответствовали частям на первом изображении.


(источник: yaroslavvb.com)

(* Visualize tree decomposition of a 3x3 grid *)

inducedGraph[set_] := Select[edges, # \[Subset] set &];
Subset[a_, b_] := (a \[Intersection] b == a);
graphName = {"Grid", {3, 3}};
edges = GraphData[graphName, "EdgeIndices"];
vars = Range[GraphData[graphName, "VertexCount"]];
vcoords = Thread[vars -> GraphData[graphName, "VertexCoordinates"]];

plotHighlight[verts_, color_] := Module[{vpos, coords},
   vpos = 
    Position[Range[GraphData[graphName, "VertexCount"]], 
     Alternatives @@ verts];
   coords = Extract[GraphData[graphName, "VertexCoordinates"], vpos];
   If[coords != {}, AppendTo[coords, First[coords] + .002]];
   Graphics[{color, CapForm["Round"], JoinForm["Round"], 
     Thickness[.2], Opacity[.3], Line[coords]}]];

jedges = {{{1, 2, 4}, {2, 4, 5, 6}}, {{2, 3, 6}, {2, 4, 5, 6}}, {{4, 
     5, 6}, {2, 4, 5, 6}}, {{4, 5, 6}, {4, 5, 6, 8}}, {{4, 7, 8}, {4, 
     5, 6, 8}}, {{6, 8, 9}, {4, 5, 6, 8}}};
jnodes = Union[Flatten[jedges, 1]];

SeedRandom[1]; colors = 
 RandomChoice[ColorData["WebSafe", "ColorList"], Length[jnodes]];
bags = MapIndexed[plotHighlight[#, bc[#] = colors[[First[#2]]]] &, 
   jnodes];
Show[bags~
  Join~{GraphPlot[Rule @@@ edges, VertexCoordinateRules -> vcoords, 
    VertexLabeling -> True]}, ImageSize -> Small]

bagCentroid[bag_] := Mean[bag /. vcoords];
findExtremeBag[vec_] := (
   vertList = First /@ vcoords;
   coordList = Last /@ vcoords;
   extremePos = 
    First[Ordering[jnodes, 1, 
      bagCentroid[#1].vec > bagCentroid[#2].vec &]];
   jnodes[[extremePos]]
   );

extremeDirs = {{1, 1}, {1, -1}, {-1, 1}, {-1, -1}};
extremeBags = findExtremeBag /@ extremeDirs;
extremePoses = bagCentroid /@ extremeBags;
vrfOuter = 
  Inset[Show[plotHighlight[#2, bc[#2]], 
     GraphPlot[Rule @@@ inducedGraph[#2], 
      VertexCoordinateRules -> vcoords, SelfLoopStyle -> None, 
      VertexLabeling -> True], ImageSize -> 100], #] &;

GraphPlot[Rule @@@ jedges, VertexRenderingFunction -> vrfOuter, 
 EdgeRenderingFunction -> ({Red, Arrowheads[0], Arrow[#1, 0]} &), 
 ImageSize -> 500, 
 VertexCoordinateRules -> Thread[Thread[extremeBags -> extremePoses]]]

Любые другие предложения для эстетически приятной визуализации графических операций приветствуются.

Ответы

Ответ 1

Вот шаги, необходимые для достижения точного контроля относительных масштабов графических объектов.

Для достижения согласованного масштаба необходимо явно указать диапазон входных координат (регулярные координаты) и диапазон выходных координат (абсолютные координаты). Обычный диапазон координат зависит от PlotRange, PlotRangePadding (и, возможно, других опций?). Абсолютный диапазон координат зависит от ImageSize, ImagePadding (и, возможно, других опций?). Для GraphPlot достаточно указать PlotRange и ImageSize.

Чтобы создать графический объект, который рендерит в заранее определенном масштабе, вам нужно выяснить PlotRange, необходимый для полного включения объекта, соответствующего ImageSize, и вернуть объект Graphics с указанными настройками. Чтобы определить необходимый PlotRange, когда задействованы толстые линии, легче иметь дело с AbsoluteThickness, назовите его abs. Чтобы полностью включить эти строки, вы можете взять наименьшее PlotRange, которое включает в себя конечные точки, затем сместить минимальные x и максимальные y границы на abs/2, и сместить максимальные x и минимальные y границы на (abs/2 + 1). Обратите внимание, что это выходные координаты.

При объединении нескольких графических объектов scale-calibrated вам необходимо пересчитать PlotRange/ImageSize и установить их явно для объединенного графического объекта.

Чтобы вставить объекты scale-calibrated в GraphPlot, необходимо убедиться, что координаты, используемые для автоматического позиционирования GraphPlot, находятся в том же диапазоне. Для этого вы можете выбрать несколько angular узлов, зафиксировать их положение вручную и разрешить автоматическое позиционирование.

Примитивы Line/JoinedCurve/FilledCurve отображают объединения/заглавные буквы по-разному в зависимости от того, является ли линия (почти) коллинеарной, поэтому необходимо вручную определять коллинеарность.

Используя этот подход, визуализированные изображения должны иметь ширину, равную

(inputPlotRange*scale + 1) + lineThickness*scale + 1

Первый дополнительный 1 предназначен для избежания "ошибки столба", а второй дополнительный 1 - это дополнительный пиксель, необходимый для добавления справа, чтобы убедиться, что толстые линии не обрезаны

Я проверил эту формулу, выполнив Rasterize для комбинированного Show и растеризовав трехмерный график с объектами, отображенными с использованием Texture и просмотренными с помощью проекции Orthographic, и она соответствует прогнозируемому результату. Делая "Копировать/Вставить" на объектах Inset в GraphPlot, а затем в Растеризации, я получаю изображение, которое на один пиксель тоньше, чем предполагалось.


(источник: yaroslavvb.com)

(**** Note, this uses JoinedCurve and Texture which are Mathematica 8 primitives.
      In Mathematica 7, JoinedCurve is not needed and can be removed *)

(** Global variables **)
scale = 50;
lineThickness = 1/2; (* line thickness in regular coordinates *)

(** Global utilities **)

(* test if 3 points are collinear, needed to work around difference \
in how colinear Line endpoints are rendered *)

collinear[points_] := 
 Length[points] == 3 && (Det[Transpose[points]~Append~{1, 1, 1}] == 0)

(* tales list of point coordinates, returns plotRange bounding box, \
uses global "scale" and "lineThickness" to get bounding box *)

getPlotRange[lst_] := (
   {xs, ys} = Transpose[lst];
   (* two extra 1/
   scale offsets needed for exact match *)
   {{Min[xs] - 
      lineThickness/2, 
     Max[xs] + lineThickness/2 + 1/scale}, {Min[ys] - 
      lineThickness/2 - 1/scale, Max[ys] + lineThickness/2}}
   );

(* Gets image size for given plot range *)

getImageSize[{{xmin_, xmax_}, {ymin_, ymax_}}] := (
   imsize = scale*{xmax - xmin, ymax - ymin} + {1, 1}
   );

(* converts plot range to vertices of rectangle *)

pr2verts[{{xmin_, xmax_}, {ymin_, ymax_}}] := {{xmin, ymin}, {xmax, 
    ymin}, {xmax, ymax}, {xmin, ymax}};

(* lifts two dimensional coordinates into 3d *)

lift[h_, coords_] := Append[#, h] & /@ coords
(* convert Raster object to array specification of texture *)

raster2texture[raster_] := Reverse[raster[[1, 1]]/255]

Subset[a_, b_] := (a \[Intersection] b == a);
inducedGraph[set_] := Select[edges, # \[Subset] set &];
values[dict_] := Map[#[[-1]] &, DownValues[dict]];


(** Graph Specific Stuff *)
graphName = {"Grid", {3, 3}};
verts = Range[GraphData[graphName, "VertexCount"]];
edges = GraphData[graphName, "EdgeIndices"];
vcoords = Thread[verts -> GraphData[graphName, "VertexCoordinates"]];
jedges = {{{1, 2, 4}, {2, 4, 5, 6}}, {{2, 3, 6}, {2, 4, 5, 6}}, {{4, 
     5, 6}, {2, 4, 5, 6}}, {{4, 5, 6}, {4, 5, 6, 8}}, {{4, 7, 8}, {4, 
     5, 6, 8}}, {{6, 8, 9}, {4, 5, 6, 8}}};
jnodes = Union[Flatten[jedges, 1]];


(* Generate diagram with explicit PlotRange,ImageSize and \
AbsoluteThickness *)
plotHL[verts_, color_] := (
   coords = verts /. vcoords;
   obj = JoinedCurve[Line[coords], 
     CurveClosed -> Not[collinear[coords]]];

   (* Figure out PlotRange and ImageSize needed to respect scale *)

    pr = getPlotRange[verts /. vcoords];
   {{xmin, xmax}, {ymin, ymax}} = pr;
   imsize = scale*{xmax - xmin, ymax - ymin};
   lineForm = {Opacity[.3], color, JoinForm["Round"], 
     CapForm["Round"], AbsoluteThickness[scale*lineThickness]};
   g = Graphics[{Directive[lineForm], obj}];
   gg = GraphPlot[Rule @@@ inducedGraph[verts], 
     VertexCoordinateRules -> vcoords];
   Show[g, gg, PlotRange -> pr, ImageSize -> imsize]
   );

(* Initialize all graph plot images *)
SeedRandom[1]; colors = 
 RandomChoice[ColorData["WebSafe", "ColorList"], Length[jnodes]];
Clear[bags];
MapThread[(bags[#1] = plotHL[#1, #2]) &, {jnodes, colors}];

(** Ploting parent graph of subgraphs **)

(* figure out coordinates of subgraphs close to edges of bounding \
box, use them to anchor parent GraphPlot *)

bagCentroid[bag_] := Mean[bag /. vcoords];
findExtremeBag[vec_] := (vertList = First /@ vcoords;
   coordList = Last /@ vcoords;
   extremePos = 
    First[Ordering[jnodes, 1, 
      bagCentroid[#1].vec > bagCentroid[#2].vec &]];
   jnodes[[extremePos]]);

extremeDirs = {{1, 1}, {1, -1}, {-1, 1}, {-1, -1}};
extremeBags = findExtremeBag /@ extremeDirs;
extremePoses = bagCentroid /@ extremeBags;

(* figure out new plot range needed to contain all objects *)

fullPR = getPlotRange[verts /. vcoords];
fullIS = getImageSize[fullPR];

(*** Show bags together merged ***)
image1 = 
 Show[values[bags], PlotRange -> fullPR, ImageSize -> fullIS]

(*** Show bags as vertices of another GraphPlot ***)
GraphPlot[
 Rule @@@ jedges,
 EdgeRenderingFunction -> ({Gray, Thick, Arrowheads[.05], 
     Arrow[#1, 0.22]} &),
 VertexCoordinateRules -> 
  Thread[Thread[extremeBags -> extremePoses]],
 VertexRenderingFunction -> (Inset[bags[#2], #] &),
 PlotRange -> fullPR,
 ImageSize -> 3*fullIS
 ]

(*** Show bags as 3d slides ***)
makeSlide[graphics_, pr_, h_] := (
  Graphics3D[{
    Texture[raster2texture[Rasterize[graphics, Background -> None]]],
    EdgeForm[None],
    Polygon[lift[h, pr2verts[pr]], 
     VertexTextureCoordinates -> pr2verts[{{0, 1}, {0, 1}}]]
    }]
  )
yoffset = 1/2;
slides = MapIndexed[
   makeSlide[bags[#], getPlotRange[# /. vcoords], 
     yoffset*First[#2]] &, jnodes];
Show[slides, ImageSize -> 3*fullIS]

(*** Show 3d slides in orthographic projection ***)
image2 = 
 Show[slides, ViewPoint -> {0, 0, Infinity}, ImageSize -> fullIS, 
  Boxed -> False]

(*** Check that 3d and 2d images rasterize to identical resolution ***)
Dimensions[Rasterize[image1][[1, 1]]] == 
 Dimensions[Rasterize[image2][[1, 1]]]

Ответ 2

Хорошо, в вашем комментарии к моему предыдущему ответу (это другой подход), вы сказали, что проблема заключалась в взаимодействии между GraphPlot/Inset/PlotRange. Если вы не укажете размер для Inset, он наследует его размер от ImageSize объекта вставки Graphics.

Здесь мое редактирование заключительного раздела в вашем первом примере, на этот раз с учетом размера графиков Inset.

(*visualize*)
vrfInner = Inset[Graphics[{White, EdgeForm[Black], Disk[{0, 0}, .05], Black, 
      Text[#2, {0, 0}]}, ImageSize -> 15], #, Center] &;
vrfOuter = Module[{edges = Rule @@@ induced[#2], prange, psize},
    prange = Union /@ Transpose[Union[Flatten[List @@@ edges]] /. vcoords];
    prange = {Min[#] - .5, Max[#] + .5} & /@ prange;
    psize = Subtract @@@ Reverse /@ prange;
    Inset[GraphPlot[edges, VertexRenderingFunction -> vrfInner, 
       VertexCoordinateRules -> vcoords, SelfLoopStyle -> None, 
       Frame -> True, ImageSize -> 100, PlotRange -> prange, 
       PlotRangePadding -> None], #, Center, Scaled[psize {.05, .04}],
       Background -> None ]] &;
TreePlot[edgesOuter, Automatic, nodes, 
 EdgeRenderingFunction -> ({Red, Arrow[#1, 0.25]} &), 
 VertexRenderingFunction -> vrfOuter, ImageSize -> 500]

alt text

n.b. {.05, .04} необходимо будет изменить, так как размер и макет внешнего графика меняются... Чтобы автоматизировать все это, вам может понадобиться хороший способ для внутренних и внешних объектов графики проверять друг друга...

Ответ 3

Вы можете исправить свой первый пример, изменив vrfOuter следующим образом:

vrfOuter =
  Inset[
    [email protected][
      [email protected]@@induced[#2],
      VertexRenderingFunction -> vrfInner,
      VertexCoordinateRules -> vcoords,
      SelfLoopStyle -> None,
      ImageSize -> {100, 100},
      AspectRatio -> 1,
      PlotRange -> {{1, 3}, {1, 3}}
    ],
    #
  ] &;

Я удалил параметр Frame- > All и добавил обратный вызов в Framed. Это потому, что я обнаружил, что не могу адекватно контролировать поля за пределами кадра, созданного первым. Мне может быть некоторая опция, но Framed работает так, как я хочу, без суеты.

Я добавил ясную высоту в параметр ImageSize. Без него Mathematica пытается выбрать высоту с использованием какого-то алгоритма, который в основном дает приятные результаты, но иногда (как здесь) путается.

Я добавил параметр AspectRatio по той же причине - Mathematica пытается выбрать "приятное" соотношение сторон (как правило, Golden Ratio), но мы этого не хотим.

Я добавил параметр PlotRange, чтобы гарантировать, что каждый подграф использует одну и ту же систему координат. Без него Mathematica обычно выбирает минимальный диапазон, который показывает все узлы.

Результаты показаны ниже. Я оставляю это как упражнение для чтения, чтобы настроить стрелки, поля и т.д.;)

rendered result

Изменить: добавлена ​​опция PlotRange в ответ на комментарий @Yaroslav Bulatov

Ответ 4

В качестве быстрого взлома вы можете ввести граф-призрак, чтобы заставить все подграфы отображаться в одной и той же сетке. Здесь моя модификация последней части вашего первого примера - мой график-призрак - это копия исходного графа, но с номерами вершин, сделанных отрицательными.

(*visualize*)

ghost = GraphData[gname, "EdgeRules"] /. HoldPattern[a_ -> b_] :> -a -> -b;
vrfInner = If[#2 > 0, 
    Inset[Graphics[{White, EdgeForm[Black], Disk[{0, 0}, .05], Black, 
       Text[#2, {0, 0}]}, ImageSize -> 15], #], {}] &;
erfInner = {If[TrueQ[#2[[1]] > 0], Blue, White], Line[#1]} &;
vrfOuter = Inset[GraphPlot[Join[Rule @@@ induced[#2], ghost],
     VertexRenderingFunction -> vrfInner, 
     VertexCoordinateRules -> (Join[#,#/.HoldPattern[a_->b_]:>-a -> b]&[vcoords]), 
     EdgeRenderingFunction -> erfInner, SelfLoopStyle -> None, 
     Frame -> True, ImageSize -> 100], #] &;
TreePlot[edgesOuter, Automatic, nodes, 
 EdgeRenderingFunction -> ({Red, Arrow[#1, 0.2]} &), 
 VertexRenderingFunction -> vrfOuter, ImageSize -> 500]

alt text

Вы можете сделать то же самое для своего второго примера. Кроме того, если вы не хотите потраченное впустую вертикальное пространство, вы можете написать быструю функцию, которая проверяет, какие узлы должны отображаться, и удерживает призраки только в необходимых строках.

Изменить: Тот же вывод можно получить, просто установив PlotRange -> {{1, 3}, {1, 3}} для внутренних графиков...