Why is it hacky?

Because we access private members (EpochMetric._*)…

What do you mean by manually ?

Idea is to setup EpochMetric like that :
def inference_step(e, batch):
    # ...
    x = batch["x"]
    y_pred = model(x)

    # compute error and properties
    err = compute_error(y,y_pred)
    x_properties = get_properties(x)

    return {
        "y_pred": y_pred,
        "err": err,
        "x_properties": x_properties,
    }

infer_engine = Engine(inference_step)

em = EpochMetric(
    compute_fn=compute_corr, 
    output_transform=lambda out: out["err"], out["x_properties"]
)
# we set em._predictions as x_properties
# and em._targets as err

em.attach(infer_engine, "overall_correlation")

@infer_engine.on(Events.COMPLETED)
def plot_infer_data(engine):
    visdom_plot_data(em._targets, em._predictions)

I didn’t check if this works, but here is the idea…

What do you mean by manually in your third sentence? Is it different from manually in your first?

There are two options to do almost the same thing. First option is to collect data manually as in the first answer. Second option is to use EpochMetric and plot data after the computation.
I would prefer the 1st option as I can control everything…