epezent / implot

Immediate Mode Plotting
MIT License
4.55k stars 503 forks source link

Force Directed Plot #500

Closed dgm3333 closed 11 months ago

dgm3333 commented 11 months ago

In case it's of any use to anyone here is basic code for a for a force directed plot

https://github.com/epezent/implot/assets/51210/07510856-5713-4644-ba73-31531a0be2a2


#include <string>
#include <vector>
#include <random>

struct fpNode {
    ImPlotPoint position;
    ImPlotPoint velocity;
    bool isBeingDragged = false;
    std::string name; // Name of the node for display on hover
};

struct fpEdge {
    int nodeA;
    int nodeB;
    float connectionStrength; // A value between 0 and 1. 1 being the strongest connection.
};
struct forcePlotStruct {

    double dt = 0.01;  // simulation time step
    double repulsionConstant = 80.0;
    double repulsionPower = 3.6;
    double attractionConstant = 125.0;
    double connectionStrengthConstant = 2.0;
    double damping = 0.9;
    float desiredDistance = 100.0f; // or whatever your desired spring length is

    std::vector<fpNode> nodes;
    std::vector<fpEdge> edges;
};

void generateForcePlotData(forcePlotStruct& forcePlot) {

    const int NUM_NODES = 30;
    const int NUM_EDGES = 100;

    // Initialize the nodes
    for (int i = 0; i < NUM_NODES; i++) {
        forcePlot.nodes.push_back(fpNode{ ImVec2(rand() % 500 - 250, rand() % 500 - 250), ImVec2(0, 0), false, "Node " + std::to_string(i + 1) });
    }

    // Initialize the edges
    std::random_device rd;
    std::mt19937 gen(rd());
    std::uniform_int_distribution<> dis(0, NUM_NODES - 1);

    for (int i = 0; i < NUM_EDGES; i++) {
        int nodeA = dis(gen);
        int nodeB = dis(gen);
        while (nodeA == nodeB) {  // Ensure we don't have an edge from a node to itself
            nodeB = dis(gen);
        }
        float strength = static_cast<float>(rand()) / static_cast<float>(RAND_MAX);  // Random strength between 0 and 1
        forcePlot.edges.push_back(fpEdge{ nodeA, nodeB, strength });
    }
}

ImU32 getColorFromStrength(float strength) {
    // Simple red-to-green gradient. You can adjust or extend this.
    ImVec4 color(1.0f - strength, strength, 0.0f, 1.0f);
    return ImColor(color);
}
//float ImLength(const ImVec2& v) {
//  return sqrt(v.x * v.x + v.y * v.y);
//}
double ImLength(ImPlotPoint& v) {
    return sqrt(v.x * v.x + v.y * v.y);
}

bool IsPointHovered(const ImPlotPoint& point, const ImPlotPoint& mousePos, float threshold = 10.0f) {

    // Check if the mouse is within the current plot
    if (!ImPlot::IsPlotHovered()) {
        return false;
    }

    // Calculate the distance between the mouse and the point
    float dx = mousePos.x - point.x;
    float dy = mousePos.y - point.y;
    float distance = sqrt(dx * dx + dy * dy);

    // Check if the distance is within the threshold
    return distance < threshold;
}

void applyForcePlotForces(forcePlotStruct& forcePlot) {

    // Repulsion between nodes
    for (auto& nodeA : forcePlot.nodes) {
        for (auto& nodeB : forcePlot.nodes) {
            if (&nodeA != &nodeB) {
                ImPlotPoint delta;
                delta.x = nodeA.position.x - nodeB.position.x;
                delta.y = nodeA.position.y - nodeB.position.y;
                double distance = ImLength(delta) + 0.01f;  // avoid division by zero
                ImPlotPoint force;
                force.x = delta.x / distance * (forcePlot.repulsionConstant / pow(distance, forcePlot.repulsionPower));
                force.y = delta.y / distance * (forcePlot.repulsionConstant / pow(distance, forcePlot.repulsionPower));
        if (std::isnan(force.x) || std::isnan(force.y)) {
            force.x = 0.0;
            force.y = 0.0;
        }
                nodeA.velocity.x -= force.x * forcePlot.dt;
                nodeA.velocity.y -= force.y * forcePlot.dt;
                nodeB.velocity.x += force.x * forcePlot.dt;
                nodeB.velocity.y += force.y * forcePlot.dt;
            }
        }
    }

    // Attraction along edges
    for (auto& edge : forcePlot.edges) {
        fpNode& nodeA = forcePlot.nodes[edge.nodeA];
        fpNode& nodeB = forcePlot.nodes[edge.nodeB];
        ImPlotPoint delta;
        delta.x = nodeA.position.x - nodeB.position.x;
        delta.y = nodeA.position.y - nodeB.position.y;
        double distance = ImLength(delta);

        // Modify desiredDistance based on connectionStrength
        double modifiedDesiredDistance = forcePlot.desiredDistance * (1.0 - edge.connectionStrength);

        // Adjusting the attraction force using attractionConstant
        double attractionMagnitude = (distance - modifiedDesiredDistance) * forcePlot.attractionConstant;
        ImPlotPoint force;
        force.x = delta.x / distance * attractionMagnitude;
        force.y = delta.y / distance * attractionMagnitude;
        if (std::isnan(force.x) || std::isnan(force.y)) {
            force.x = 0.0;
            force.y = 0.0;
        }
        nodeA.velocity.x -= force.x * forcePlot.dt;
        nodeA.velocity.y -= force.y * forcePlot.dt;
        nodeB.velocity.x += force.x * forcePlot.dt;
        nodeB.velocity.y += force.y * forcePlot.dt;
    }

    // Damping & update positions
    for (auto& node : forcePlot.nodes) {
        node.velocity.x *= forcePlot.damping;
        node.velocity.y *= forcePlot.damping;

        // NaN Protection for velocities
        if (std::isnan(node.velocity.x) || std::isinf(node.velocity.x))
            node.velocity.x = 0.0;
        if (std::isnan(node.velocity.y) || std::isinf(node.velocity.y))
            node.velocity.y = 0.0;

        node.position.x += node.velocity.x * forcePlot.dt;
        node.position.y += node.velocity.y * forcePlot.dt;
    }
}

void renderForcePlot(forcePlotStruct& forcePlot) {

    static int draggedNodeIndex = -1;  // store the index of the dragged node

    // Plot nodes and edges using ImPlot

    if (ImPlot::BeginPlot("##ForceDirected", nullptr, nullptr, ImVec2(-1, -1), 0, 0, 0)) {

        ImPlot::SetupAxes("x", "y", ImPlotAxisFlags_NoInitialFit, ImPlotAxisFlags_NoInitialFit);

        // axes 10% larger than the outermost node
        double x_min = DBL_MAX, x_max = DBL_MIN, y_min = DBL_MAX, y_max = DBL_MIN;
        for (const auto& node : forcePlot.nodes) {
            x_min = std::min(x_min, node.position.x);
            x_max = std::max(x_max, node.position.x);
            y_min = std::min(y_min, node.position.y);
            y_max = std::max(y_max, node.position.y);
        }
        if (x_min < 0)
            x_min *= 1.1;
        else
            x_min *= 0.9;
        if (x_max < 0)
            x_max *= 0.9;
        else
            x_max *= 1.1;
        if (y_min < 0)
            y_min *= 1.1;
        else
            y_min *= 0.9;
        if (y_max < 0)
            y_max *= 0.9;
        else
            y_max *= 1.1;

        ImPlot::SetupAxesLimits(x_min, x_max, y_min, y_max, ImPlotCond_Always);

        // Mouse interaction logic
        // Calculate mouse position in ImPlot's coordinate space
        ImPlotPoint mousePlotPos = ImPlot::GetPlotMousePos();

        if (ImPlot::IsPlotHovered()) {
            if (ImGui::IsMouseClicked(0)) {  // Check for left mouse button press
                // Search for a node under the cursor
                for (size_t i = 0; i < forcePlot.nodes.size(); i++) {
                    ImPlotPoint delta;
                    delta.x = forcePlot.nodes[i].position.x - mousePlotPos.x;
                    delta.y = forcePlot.nodes[i].position.y - mousePlotPos.y;
                    if (ImLength(delta) < 10.0f) {  // Arbitrary distance for selecting nodes, adjust as needed
                        forcePlot.nodes[i].isBeingDragged = true;
                        draggedNodeIndex = i;
                        break;
                    }
                }
            }
            else if (ImGui::IsMouseReleased(0) && draggedNodeIndex != -1) {
                forcePlot.nodes[draggedNodeIndex].isBeingDragged = false;
                draggedNodeIndex = -1;  // reset dragged node index
            }
        }

        if (true) {

            applyForcePlotForces(forcePlot);  // Only apply forces if no node is being dragged
            // If node is being dragged, update its position to the mouse's plot position
            if (draggedNodeIndex != -1) {
                forcePlot.nodes[draggedNodeIndex].position = mousePlotPos;
                forcePlot.nodes[draggedNodeIndex].velocity = ImVec2(0, 0);  // Reset velocity so it doesn't "spring" back
            }
        }
        else {
            // If node is being dragged, update its position to the mouse's plot position
            if (draggedNodeIndex != -1) {
                forcePlot.nodes[draggedNodeIndex].position = mousePlotPos;
                forcePlot.nodes[draggedNodeIndex].velocity = ImVec2(0, 0);  // Reset velocity so it doesn't "spring" back
            }
            else {
                applyForcePlotForces(forcePlot);  // Only apply forces if no node is being dragged
            }
        }

        //ImPlot::SetPlotLimits(-500, 500, -500, 500);
        for (const auto& edge : forcePlot.edges) {
            double linex[] = { forcePlot.nodes[edge.nodeA].position.x, forcePlot.nodes[edge.nodeB].position.x };
            double liney[] = { forcePlot.nodes[edge.nodeA].position.y, forcePlot.nodes[edge.nodeB].position.y };
            ImU32 edgeColor = getColorFromStrength(edge.connectionStrength);
            ImPlot::PushStyleColor(ImPlotCol_Line, edgeColor); // Set the edge color
            ImPlot::PlotLine("Edge", linex, liney, 2);
            ImPlot::PopStyleColor(); // Reset to default
        }

        for (const auto& node : forcePlot.nodes) {
            ImPlot::PlotScatter("Node", &node.position.x, &node.position.y, 1);

            // Check for hover and display name
            //ImPlotPoint hoveredNode = ImPlot::PixelsToPlot((const ImVec2)node.position);
            if (IsPointHovered(node.position, mousePlotPos)) {
                ImGui::BeginTooltip();
                ImGui::Text("%s", node.name.c_str());
                ImGui::EndTooltip();
            }

        }

        ImPlot::EndPlot();
    }

}

void Demo_ForceDirectedPlots() {

    static forcePlotStruct forcePlot;

    static bool needsInit = true;
    if (needsInit) {
        needsInit = false;
        generateForcePlotData(forcePlot);
    }

    if (ImGui::Button("Reset")) {
        forcePlot = forcePlotStruct();
        generateForcePlotData(forcePlot);
    }

    // Slider for dt
    static float dt = (float)forcePlot.dt;
    if (ImGui::SliderFloat("Time Step (dt)", &dt, 0.001, 0.1)) {
        forcePlot.dt = dt;
    }

    // Slider for repulsionConstant
    static float repulsionConstant = (float)forcePlot.repulsionConstant;
    if (ImGui::SliderFloat("Repulsion Constant", &repulsionConstant, 0.0, 1000.0)) {
        forcePlot.repulsionConstant = repulsionConstant;
    }

    // Slider for repulsionPower
    static float repulsionPower = (float)forcePlot.repulsionPower;
    if (ImGui::SliderFloat("Repulsion Power", &repulsionPower, 0.0, 10.0)) {
        forcePlot.repulsionPower = repulsionPower;
    }

    // Slider for attractionConstant
    static float attractionConstant = (float)forcePlot.attractionConstant;
    if (ImGui::SliderFloat("Attraction Constant", &attractionConstant, 0.0, 1000.0)) {
        forcePlot.attractionConstant = attractionConstant;
    }

    // Slider for connectionStrengthConstant
    static float connectionStrengthConstant = (float)forcePlot.connectionStrengthConstant;
    if (ImGui::SliderFloat("connection Strength Constant", &connectionStrengthConstant, 0.0, 10.0)) {
        forcePlot.connectionStrengthConstant = connectionStrengthConstant;
    }

    // Slider for desiredDistance
    static float desiredDistance = (float)forcePlot.desiredDistance;
    if (ImGui::SliderFloat("Desired Distance", &desiredDistance, 0.0, 1000.0)) {
        forcePlot.desiredDistance = desiredDistance;
    }

    // Slider for damping
    static float damping = (float)forcePlot.damping;
    if (ImGui::SliderFloat("Damping", &damping, 0.0, 5.0)) {
        forcePlot.damping = damping;
    }

    renderForcePlot(forcePlot);

}
epezent commented 10 months ago

This is pretty awesome.

noncom commented 3 months ago

Just found this. Shouldn't it be added to the demos maybe? While I understand that the demos are more about showcasing various basic elements of implot, something like this definitely needs a better visibility for the new/potential users. So having it as a demo or in some kind of a snippet library would be quite handy?

Solved issues is not a place where I'd think to look for such examples... I've tried googling "implot force directed plot" and "implot force directed plot github" right now, and this issue is not among the search results.

dgm3333 commented 3 months ago

I don't disagree as it would be nice to have a library of such things. It's unfortunately not something I have time to tidy up for general consumption in the next few months as I'm totally swamped. But I'll have a look at my current code base as I think I made some (relatively minor) updates to this version.